Merge branch 'master' into github-poc

This commit is contained in:
MrCloudSec committed 2024-11-19 12:35:50 -04:00
commit b815eeb7ab
115 files changed
+3868 -1830

No files matched your search

+7
View File
@@ -79,3 +79,10 @@ output/csv:
- changed-files:
- any-glob-to-any-file: "prowler/lib/outputs/csv/**"
- any-glob-to-any-file: "tests/lib/outputs/csv/**"
compliance:
- changed-files:
- any-glob-to-any-file: "prowler/lib/outputs/compliance/**"
- any-glob-to-any-file: "tests/lib/outputs/compliance/**"
- any-glob-to-any-file: "prowler/compliance/**"
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
with:
fetch-depth: 0
- name: TruffleHog OSS
uses: trufflesecurity/trufflehog@v3.83.6
uses: trufflesecurity/trufflehog@v3.83.7
with:
path: ./
base: ${{ github.event.repository.default_branch }}
+1 -1
View File
@@ -91,6 +91,6 @@ jobs:
poetry run pytest -n auto --cov=./prowler --cov-report=xml tests
- name: Upload coverage reports to Codecov
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+1 -1
View File
@@ -4,7 +4,7 @@ LABEL maintainer="https://github.com/prowler-cloud/prowler"
# Update system dependencies and install essential tools
#hadolint ignore=DL3018
RUN apk --no-cache upgrade && apk --no-cache add curl git g++
RUN apk --no-cache upgrade && apk --no-cache add curl git
# Create non-root user
RUN mkdir -p /home/prowler && \
-2
View File
@@ -30,8 +30,6 @@ The following list includes all the AWS checks with configurable variables that
| `cloudtrail_threat_detection_privilege_escalation` | `threat_detection_privilege_escalation_entropy` | Integer |
| `cloudtrail_threat_detection_privilege_escalation` | `threat_detection_privilege_escalation_minutes` | Integer |
| `cloudwatch_log_group_no_secrets_in_logs` | `secrets_ignore_patterns` | List of Strings |
| `cloudwatch_log_group_no_critical_pii_in_logs` | `critical_pii_entities` | List of Strings |
| `cloudwatch_log_group_no_critical_pii_in_logs` | `pii_language` | String |
| `cloudwatch_log_group_retention_policy_specific_days_enabled` | `log_group_retention_days` | Integer |
| `codebuild_project_no_secrets_in_variables` | `excluded_sensitive_environment_variables` | List of Strings |
| `codebuild_project_no_secrets_in_variables` | `secrets_ignore_patterns` | List of Strings |
+36
View File
@@ -0,0 +1,36 @@
# Prowler Check Kreator
???+ note
Currently, this tool is only available for creating checks for the AWS provider.
**Prowler Check Kreator** is a utility designed to streamline the creation of new checks for Prowler. This tool generates all necessary files required to add a new check to the Prowler repository. Specifically, it creates:
- A dedicated folder for the check.
- The main check script.
- A metadata file with essential details.
- A folder and file structure for testing the check.
## Usage
To use the tool, execute the main script with the following command:
```bash
python util/prowler_check_kreator/prowler_check_kreator.py <prowler_provider> <check_name>
```
Parameters:
- `<prowler_provider>`: Currently only AWS is supported.
- `<check_name>`: The name you wish to assign to the new check.
## AI integration
This tool optionally integrates AI to assist in generating the check code and metadata file content. When AI assistance is chosen, the tool uses [Gemini](https://gemini.google.com/) to produce preliminary code and metadata.
???+ note
For this feature to work, you must have the library `google-generativeai` installed in your Python environment.
???+ warning
AI-generated code and metadata might contain errors or require adjustments to align with specific Prowler requirements. Carefully review all AI-generated content before committing.
To enable AI assistance, simply confirm when prompted by the tool. Additionally, ensure that the `GEMINI_API_KEY` environment variable is set with a valid Gemini API key. For instructions on obtaining your API key, refer to the [Gemini documentation](https://ai.google.dev/gemini-api/docs/api-key).
+1
View File
@@ -65,6 +65,7 @@ nav:
- Pentesting: tutorials/pentesting.md
- Parallel Execution: tutorials/parallel-execution.md
- Developer Guide: developer-guide/introduction.md
- Prowler Check Kreator: tutorials/prowler-check-kreator.md
- AWS:
- Authentication: tutorials/aws/authentication.md
- Assume Role: tutorials/aws/role-assumption.md
@@ -91,6 +91,8 @@ Resources:
- 'shield:GetSubscriptionState'
- 'securityhub:BatchImportFindings'
- 'securityhub:GetFindings'
- 'servicecatalog:Describe*'
- 'servicecatalog:List*'
- 'ssm:GetDocument'
- 'ssm-incidents:List*'
- 'support:Describe*'
@@ -39,6 +39,8 @@
"shield:GetSubscriptionState",
"securityhub:BatchImportFindings",
"securityhub:GetFindings",
"servicecatalog:Describe*",
"servicecatalog:List*",
"ssm:GetDocument",
"ssm-incidents:List*",
"support:Describe*",
Generated
+186 -862
View File
File diff suppressed because it is too large. Load diff
+5 -25
View File
@@ -72,31 +72,6 @@ aws:
# AWS Cloudwatch Configuration
# aws.cloudwatch_log_group_retention_policy_specific_days_enabled --> by default is 365 days
log_group_retention_days: 365
# aws.cloudwatch_log_group_no_critical_pii_in_logs --> see all available entities in https://microsoft.github.io/presidio/supported_entities/
critical_pii_entities : [
"CREDIT_CARD", # Credit card numbers are highly sensitive financial information.
"CRYPTO", # Crypto wallet numbers (e.g., Bitcoin addresses) can give access to cryptocurrency.
"IBAN_CODE", # International Bank Account Numbers are critical financial information.
"US_BANK_NUMBER", # US bank account numbers are sensitive and should be protected.
"US_SSN", # US Social Security Numbers are critical PII used for identity verification.
"US_PASSPORT", # US passport numbers can be used for identity theft.
"US_ITIN", # US Individual Taxpayer Identification Numbers are sensitive personal identifiers.
#"UK_NHS", # UK NHS numbers can be used to access medical records and other private information.
#"ES_NIF", # Spanish NIF (Personal tax ID) is critical for identification and tax purposes.
#"ES_NIE", # Spanish NIE (Foreigners ID card) is a critical identifier for foreign residents.
#"IT_FISCAL_CODE", # Italian personal identification code is sensitive PII for tax and legal purposes.
#"IT_PASSPORT", # Italian passport numbers are critical PII.
#"IT_IDENTITY_CARD", # Italian identity card numbers are critical for personal identification.
#"PL_PESEL", # Polish PESEL numbers are sensitive personal identifiers.
#"SG_NRIC_FIN", # Singapore National Registration Identification Card is critical PII.
#"AU_ABN", # Australian Business Numbers are critical for business identification.
#"AU_TFN", # Australian Tax File Numbers are sensitive and used for taxation purposes.
#"AU_MEDICARE", # Australian Medicare numbers are sensitive medical identifiers.
#"IN_PAN", # Indian Permanent Account Numbers are critical for tax purposes and identity.
#"IN_AADHAAR", # Indian Aadhaar numbers are highly sensitive and serve as a universal identity number.
#"FI_PERSONAL_IDENTITY_CODE" # Finnish Personal Identity Code is sensitive PII for personal identification.
]
pii_language: "en" # Language for recognizing PII entities
# AWS AppStream Session Configuration
# aws.appstream_fleet_session_idle_disconnect_timeout
@@ -393,6 +368,11 @@ aws:
# Maximum number of days a secret should be rotated
max_days_secret_unrotated: 90
# AWS Kinesis Configuration
# Minimum retention period in hours for Kinesis streams
min_kinesis_stream_retention_hours: 168 # 7 days
# Azure Configuration
azure:
# Azure Network Configuration
@@ -42,7 +42,7 @@ class AzureCIS(ComplianceOutput):
compliance_row = AzureCISModel(
Provider=finding.provider,
Description=compliance.Description,
Subscription=finding.account_name,
SubscriptionId=finding.account_uid,
Location=finding.region,
AssessmentDate=str(finding.timestamp),
Requirements_Id=requirement.Id,
@@ -73,7 +73,7 @@ class AzureCIS(ComplianceOutput):
compliance_row = AzureCISModel(
Provider=compliance.Provider.lower(),
Description=compliance.Description,
Subscription="",
SubscriptionId="",
Location="",
AssessmentDate=str(finding.timestamp),
Requirements_Id=requirement.Id,
+1 -1
View File
@@ -38,7 +38,7 @@ class AzureCISModel(BaseModel):
Provider: str
Description: str
Subscription: str
SubscriptionId: str
Location: str
AssessmentDate: str
Requirements_Id: str
+242 -31
View File
@@ -39,6 +39,7 @@ from prowler.providers.aws.exceptions.exceptions import (
AWSIAMRoleARNPartitionEmptyError,
AWSIAMRoleARNRegionNotEmtpyError,
AWSIAMRoleARNServiceNotIAMnorSTSError,
AWSInvalidPartitionError,
AWSInvalidProviderIdError,
AWSNoCredentialsError,
AWSProfileNotFoundError,
@@ -62,12 +63,32 @@ from prowler.providers.aws.models import (
AWSMFAInfo,
AWSOrganizationsInfo,
AWSSession,
Partition,
)
from prowler.providers.common.models import Audit_Metadata, Connection
from prowler.providers.common.provider import Provider
class AwsProvider(Provider):
"""
AwsProvider class is the main class for the AWS provider.
This class is responsible for initializing the AWS provider, setting up the AWS session, validating the AWS
credentials, assuming an IAM role, getting the AWS Organizations metadata, and setting the AWS identity.
Attributes:
_type (str): The provider type.
_identity (AWSIdentityInfo): The AWS provider identity information.
_session (AWSSession): The AWS provider session.
_organizations_metadata (AWSOrganizationsInfo): The AWS Organizations metadata.
_audit_resources (list): The list of resources to audit.
_audit_config (dict): The audit configuration.
_scan_unused_services (bool): A boolean indicating whether to scan unused services.
_enabled_regions (set): The set of enabled regions.
_mutelist (AWSMutelist): The AWS provider mutelist.
audit_metadata (Audit_Metadata): The audit metadata.
"""
_type: str = "aws"
_identity: AWSIdentityInfo
_session: AWSSession
@@ -106,10 +127,10 @@ class AwsProvider(Provider):
"""
Initializes the AWS provider.
Arguments:
Args:
- retries_max_attempts: The maximum number of retries for the AWS client.
- role_arn: The ARN of the IAM role to assume.
- session_duration: The duration of the session in seconds.
- session_duration: The duration of the session in seconds, between 900 and 43200.
- external_id: The external ID to use when assuming the IAM role.
- role_session_name: The name of the session when assuming the IAM role.
- mfa: A boolean indicating whether MFA is enabled.
@@ -134,6 +155,37 @@ class AwsProvider(Provider):
- ArgumentTypeError: If the input external ID is invalid.
- ArgumentTypeError: If the input role session name is invalid.
Usage:
- Boto3 is used so we follow their credential setup process:
- Authentication: Make sure you have properly configured your AWS CLI with a valid Access Key and Region or declare the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables.
- aws configure
or
- export AWS_ACCESS_KEY_ID="ASXXXXXXX"
export AWS_SECRET_ACCESS_KEY="XXXXXXXXX"
export AWS_SESSION_TOKEN="XXXXXXXXX"
- To create a new aws object you can use:
- aws = AwsProvider()
- aws = AwsProvider(aws_access_key_id="ASXXXXXXX", aws_secret_access_key="XXXXXXXXX", aws_session_token="XXXXXXXXX")
- Profile: If you have multiple profiles in your AWS CLI configuration, you can specify the profile to use:
- aws = AwsProvider(profile="profile_name")
- MFA: If you have MFA enabled you can specify it:
- aws = AwsProvider(mfa=True)
* Note: If you have MFA enabled you will be prompted to enter the MFA ARN and the MFA TOTP code.
* Note: Take into account that you can use static credentials or a profile, with the combination of MFA.
- Assume Role: *Requires authentication.* Prowler can be used against multiple accounts using IAM Assume Role features depending on each use case:
- Set up a custom profile inside your AWS CLI configuration file:
- [profile profile_name]
role_arn = arn:aws:iam::123456789012:role/role_name
- aws = AwsProvider(profile="profile_name")
- Use role_arn directly:
- aws = AwsProvider(role_arn="arn:aws:iam::123456789012:role/role_name")
- Use role_arn with session duration(in seconds, by default 3600) and external ID:
- aws = AwsProvider(role_arn="arn:aws:iam::123456789012:role/role_name", session_duration=3600, external_id="external_id")
- Use custom role session name:
- aws = AwsProvider(role_arn="arn:aws:iam::123456789012:role/role_name", role_session_name="custom_session_name")
* Note: You can use the combination of MFA with Assume Role.
- aws = AwsProvider(role_arn="arn:aws:iam::123456789012:role/role_name", mfa=True)
"""
logger.info("Initializing AWS provider ...")
@@ -367,7 +419,7 @@ class AwsProvider(Provider):
"""
get_organizations_info returns a AWSOrganizationsInfo object if the account to be audited is a delegated administrator for AWS Organizations or if the AWS Organizations Role ARN (--organizations-role) is passed.
Arguments:
Args:
- organizations_session: needs to be a Session object with permissions to do organizations:DescribeAccount and organizations:ListTagsForResource.
- aws_account_id: is the AWS Account ID from which we want to get the AWS Organizations account metadata
@@ -424,6 +476,21 @@ class AwsProvider(Provider):
regions: set,
profile_region: str,
) -> AWSIdentityInfo:
"""
set_identity sets the AWS provider identity information.
Args:
- caller_identity: The AWS caller identity information.
- profile: The AWS CLI profile name.
- regions: A set of regions to audit.
- profile_region: The AWS CLI profile region.
Returns:
- AWSIdentityInfo: The AWS provider identity information.
Raises:
- AWSInvalidProviderIdError: If the AWS provider ID is invalid.
"""
logger.info(f"Original AWS Caller Identity UserId: {caller_identity.user_id}")
logger.info(f"Original AWS Caller Identity ARN: {caller_identity.arn}")
@@ -447,6 +514,22 @@ class AwsProvider(Provider):
aws_secret_access_key: str = None,
aws_session_token: Optional[str] = None,
) -> Session:
"""
setup_session sets up an AWS session using the provided credentials.
Args:
- mfa: A boolean indicating whether MFA is enabled.
- profile: The name of the AWS CLI profile to use.
- aws_access_key_id: The AWS access key ID.
- aws_secret_access_key: The AWS secret access key.
- aws_session_token: The AWS session token, optional.
Returns:
- Session: The AWS session.
Raises:
- AWSSetUpSessionError: If an error occurs during the setup process.
"""
try:
logger.debug("Creating original session ...")
@@ -588,6 +671,21 @@ class AwsProvider(Provider):
return refreshed_credentials
def print_credentials(self):
"""
Print the AWS credentials.
This method prints the AWS credentials used by the provider.
Example output:
```
Using the AWS credentials below:
AWS-CLI Profile: default
AWS Regions: all
AWS Account: 123456789012
User Id: AIDAJDPLRKLG7EXAMPLE
Caller Identity ARN: arn:aws:iam::123456789012:user/prowler
```
"""
# Beautify audited regions, set "all" if there is no filter region
regions = (
", ".join(self._identity.audited_regions)
@@ -659,7 +757,7 @@ class AwsProvider(Provider):
"""
get_available_aws_service_regions returns the available regions for the given service and partition.
Arguments:
Args:
- service: The AWS service name.
- partition: The AWS partition name. Default is "aws".
- audited_regions: A set of regions to audit. Default is None.
@@ -679,6 +777,12 @@ class AwsProvider(Provider):
def get_checks_from_input_arn(self) -> set:
"""
get_checks_from_input_arn gets the list of checks from the input arns
Returns:
- set: set of strings representing the checks from the input arns
Example:
checks = get_checks_from_input_arn()
"""
checks_from_arn = set()
is_subservice_in_checks = False
@@ -745,7 +849,18 @@ class AwsProvider(Provider):
# TODO: This can be moved to another class since it doesn't need self
def get_regions_from_audit_resources(self, audit_resources: list) -> set:
"""get_regions_from_audit_resources gets the regions from the audit resources arns"""
"""get_regions_from_audit_resources gets the regions from the audit resources arns
Args:
- audit_resources: list of ARNs of the resources to audit
Returns:
- set: set of strings representing the regions from the audit resources arns
Example:
audit_resources = ["arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0"]
regions = get_regions_from_audit_resources(audit_resources)
"""
audited_regions = set()
for resource in audit_resources:
region = resource.split(":")[3]
@@ -805,7 +920,18 @@ class AwsProvider(Provider):
raise error
def get_default_region(self, service: str) -> str:
"""get_default_region returns the default region based on the profile and audited service regions"""
"""get_default_region returns the default region based on the profile and audited service regions
Args:
- service: The AWS service name
Returns:
- str: The default region for the given service
Example:
service = "ec2"
default_region = get_default_region(service)
"""
try:
service_regions = AwsProvider.get_available_aws_service_regions(
service, self._identity.partition, self._identity.audited_regions
@@ -826,7 +952,14 @@ class AwsProvider(Provider):
raise error
def get_global_region(self) -> str:
"""get_global_region returns the global region based on the audited partition"""
"""get_global_region returns the global region based on the audited partition
Returns:
- str: The global region for the audited partition
Example:
global_region = get_global_region()a
"""
global_region = "us-east-1"
if self._identity.partition == "aws-cn":
global_region = "cn-north-1"
@@ -838,7 +971,14 @@ class AwsProvider(Provider):
@staticmethod
def input_role_mfa_token_and_code() -> AWSMFAInfo:
"""input_role_mfa_token_and_code ask for the AWS MFA ARN and TOTP and returns it."""
"""input_role_mfa_token_and_code ask for the AWS MFA ARN and TOTP and returns it.
Returns:
- AWSMFAInfo: An object containing the MFA ARN and TOTP code
Example:
mfa_info = input_role_mfa_token_and_code()
"""
mfa_ARN = input("Enter ARN of MFA: ")
mfa_TOTP = input("Enter MFA code: ")
return AWSMFAInfo(arn=mfa_ARN, totp=mfa_TOTP)
@@ -846,6 +986,12 @@ class AwsProvider(Provider):
def set_session_config(self, retries_max_attempts: int) -> Config:
"""
set_session_config returns a botocore Config object with the Prowler user agent and the default retrier configuration if nothing is passed as argument
Args:
- retries_max_attempts: The maximum number of retries for the standard retrier config
Returns:
- Config: The botocore Config object
"""
# Set the maximum retries for the standard retrier config
default_session_config = Config(
@@ -872,6 +1018,13 @@ class AwsProvider(Provider):
) -> AWSCredentials:
"""
assume_role assumes the IAM roles passed with the given session and returns AWSCredentials
Args:
- session: The AWS session object
- assumed_role_info: The AWSAssumeRoleInfo object
Returns:
- AWSCredentials: The AWS credentials for the assumed role
"""
try:
role_session_name = (
@@ -923,7 +1076,14 @@ class AwsProvider(Provider):
)
def get_aws_enabled_regions(self, current_session: Session) -> set:
"""get_aws_enabled_regions returns a set of enabled AWS regions"""
"""get_aws_enabled_regions returns a set of enabled AWS regions
Args:
- current_session: The AWS session object
Returns:
- set: set of strings representing the enabled AWS regions
"""
try:
# EC2 Client to check enabled regions
service = "ec2"
@@ -947,6 +1107,12 @@ class AwsProvider(Provider):
# TODO: review this function
# Maybe this should be done within the AwsProvider and not in __main__.py
def get_checks_to_execute_by_audit_resources(self) -> set[str]:
"""
get_checks_to_execute_by_audit_resources gets the checks to execute based on the audit resources
Returns:
- set: set of strings representing the checks to execute
"""
# Once the provider is set and we have the eventual checks from arn, it is time to exclude the others
try:
checks = set()
@@ -1273,7 +1439,7 @@ class AwsProvider(Provider):
"""
Create an STS session client.
Parameters:
Args:
- session (session.Session): The AWS session object.
- aws_region (str): The AWS region to use for the session.
@@ -1297,6 +1463,59 @@ class AwsProvider(Provider):
)
raise error
@staticmethod
def get_regions(partition: Partition = Partition.aws) -> set:
"""
Get the available AWS regions from the AWS services JSON file with the ability of filtering by partition.
Args:
partition (str): The AWS partition to retrieve regions for. Defaults to "aws".
Returns:
set: A set of region names.
Raises:
AWSInvalidPartitionError: If the provided partition name is invalid.
Example:
>>> AwsProvider.get_regions("aws")
{"af-south-1"}
"""
try:
regions = set()
data = read_aws_regions_file()
if partition is None:
for service in data["services"].values():
for partition in service["regions"]:
regions.update(service["regions"][partition])
else:
partition = Partition(partition)
for service in data["services"].values():
regions.update(service["regions"][partition.value])
return regions
except ValueError as value_error:
logger.error(
f"{value_error.__class__.__name__}[{value_error.__traceback__.tb_lineno}]: {value_error}"
)
raise AWSInvalidPartitionError(
message=f"Invalid partition: {partition}",
file=os.path.basename(__file__),
)
except KeyError as key_error:
logger.error(
f"{key_error.__class__.__name__}[{key_error.__traceback__.tb_lineno}]: {key_error}"
)
raise AWSInvalidPartitionError(
message=f"Invalid partition: {partition}",
file=os.path.basename(__file__),
)
except Exception as error:
logger.error(f"{error.__class__.__name__}: {error}")
raise error
def read_aws_regions_file() -> dict:
"""
@@ -1313,29 +1532,21 @@ def read_aws_regions_file() -> dict:
return data
def get_aws_available_regions() -> set:
"""
Get the available AWS regions from the AWS services JSON file.
Returns:
set: A set of available AWS regions.
"""
try:
data = read_aws_regions_file()
regions = set()
for service in data["services"].values():
for partition in service["regions"]:
for item in service["regions"][partition]:
regions.add(item)
return regions
except Exception as error:
logger.error(f"{error.__class__.__name__}: {error}")
return set()
# TODO: This can be moved to another class since it doesn't need self
def get_aws_region_for_sts(session_region: str, regions: set[str]) -> str:
"""
Get the AWS region for the STS Assume Role operation.
Args:
- session_region (str): The region configured in the AWS session.
- regions (set[str]): The regions passed with the -f/--region/--filter-region option.
Returns:
str: The AWS region for the STS Assume Role operation
Example:
aws_region = get_aws_region_for_sts(session_region, regions)
"""
# If there is no region passed with -f/--region/--filter-region
if regions is None or len(regions) == 0:
# If you have a region configured in your AWS config or credentials file
@@ -2627,6 +2627,7 @@
"connectcampaigns": {
"regions": {
"aws": [
"af-south-1",
"ap-southeast-2",
"ca-central-1",
"eu-central-1",
@@ -3979,6 +3980,7 @@
"ap-northeast-2",
"ap-northeast-3",
"ap-south-1",
"ap-south-2",
"ap-southeast-1",
"ap-southeast-2",
"ap-southeast-3",
@@ -3991,6 +3993,7 @@
"eu-west-1",
"eu-west-2",
"eu-west-3",
"il-central-1",
"me-central-1",
"me-south-1",
"sa-east-1",
@@ -4839,6 +4842,7 @@
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -5549,6 +5553,7 @@
"iotfleetwise": {
"regions": {
"aws": [
"ap-south-1",
"eu-central-1",
"us-east-1"
],
@@ -5855,6 +5860,7 @@
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -5971,6 +5977,7 @@
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -7160,6 +7167,7 @@
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -7623,14 +7631,9 @@
"opsworkscm": {
"regions": {
"aws": [
"ap-southeast-1",
"ap-southeast-2",
"eu-central-1",
"eu-west-1",
"us-east-1",
"us-east-2",
"us-west-1",
"us-west-2"
"us-east-1"
],
"aws-cn": [],
"aws-us-gov": []
@@ -9852,6 +9855,7 @@
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -10684,7 +10688,10 @@
"us-east-2",
"us-west-2"
],
"aws-cn": [],
"aws-cn": [
"cn-north-1",
"cn-northwest-1"
],
"aws-us-gov": []
}
},
@@ -10770,6 +10777,7 @@
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -11088,6 +11096,7 @@
"ap-south-1",
"ap-southeast-1",
"ap-southeast-2",
"ap-southeast-3",
"ca-central-1",
"eu-central-1",
"eu-north-1",
@@ -74,6 +74,10 @@ class AWSBaseException(ProwlerException):
"message": "The provided AWS Session Token is expired",
"remediation": "Get a new AWS Session Token and configure it for the provider.",
},
(1917, "AWSInvalidPartitionError"): {
"message": "The provided AWS partition is invalid",
"remediation": "Check the provided AWS partition and ensure it is valid.",
},
}
def __init__(self, code, file=None, original_exception=None, message=None):
@@ -220,3 +224,10 @@ class AWSSessionTokenExpiredError(AWSCredentialsError):
super().__init__(
1016, file=file, original_exception=original_exception, message=message
)
class AWSInvalidPartitionError(AWSBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
1917, file=file, original_exception=original_exception, message=message
)
@@ -1,7 +1,7 @@
from argparse import ArgumentTypeError, Namespace
from re import fullmatch, search
from prowler.providers.aws.aws_provider import get_aws_available_regions
from prowler.providers.aws.aws_provider import AwsProvider
from prowler.providers.aws.config import ROLE_SESSION_NAME
from prowler.providers.aws.lib.arn.arn import arn_type
@@ -64,7 +64,7 @@ def init_parser(self):
"-f",
nargs="+",
help="AWS region names to run Prowler against",
choices=get_aws_available_regions(),
choices=AwsProvider.get_regions(partition=None),
)
# AWS Organizations
aws_orgs_subparser = aws_parser.add_argument_group("AWS Organizations")
+24
View File
@@ -1,5 +1,6 @@
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from boto3.session import Session
from botocore.config import Config
@@ -82,6 +83,29 @@ class AWSMFAInfo:
totp: str
class Partition(str, Enum):
"""
Enum class representing different AWS partitions.
Attributes:
aws (str): Represents the standard AWS commercial regions.
aws_cn (str): Represents the AWS China regions.
aws_us_gov (str): Represents the AWS GovCloud (US) Regions.
aws_iso (str): Represents the AWS ISO (US) Regions.
aws_iso_b (str): Represents the AWS ISOB (US) Regions.
aws_iso_e (str): Represents the AWS ISOE (Europe) Regions.
aws_iso_f (str): Represents the AWS ISOF Regions.
"""
aws = "aws"
aws_cn = "aws-cn"
aws_us_gov = "aws-us-gov"
aws_iso = "aws-iso"
aws_iso_b = "aws-iso-b"
aws_iso_e = "aws-iso-e"
aws_iso_f = "aws-iso-f"
class AWSOutputOptions(ProviderOutputOptions):
security_hub_enabled: bool
@@ -25,7 +25,9 @@
"Url": "https://aws.amazon.com/blogs/security/how-to-get-ready-for-certificate-transparency/"
}
},
"Categories": [],
"Categories": [
"logging"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -29,7 +29,8 @@
}
},
"Categories": [
"forensics-ready"
"forensics-ready",
"logging"
],
"DependsOn": [],
"RelatedTo": [],
@@ -28,7 +28,9 @@
"Url": "https://docs.aws.amazon.com/apigateway/latest/developerguide/security-monitoring.html"
}
},
"Categories": [],
"Categories": [
"logging"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -24,7 +24,8 @@
}
},
"Categories": [
"forensics-ready"
"forensics-ready",
"logging"
],
"DependsOn": [],
"RelatedTo": [],
@@ -24,7 +24,8 @@
}
},
"Categories": [
"forensics-ready"
"forensics-ready",
"logging"
],
"DependsOn": [],
"RelatedTo": [],
@@ -28,7 +28,8 @@
}
},
"Categories": [
"forensics-ready"
"forensics-ready",
"logging"
],
"DependsOn": [],
"RelatedTo": [],
@@ -1,32 +0,0 @@
{
"Provider": "aws",
"CheckID": "cloudwatch_log_group_no_critical_pii_in_logs",
"CheckTitle": "Check if secrets exists in CloudWatch logs.",
"CheckType": [],
"ServiceName": "cloudwatch",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:cloudwatch:region:account-id:log-group/resource-id",
"Severity": "medium",
"ResourceType": "Other",
"Description": "Check if secrets exists in CloudWatch logs",
"Risk": "Storing sensitive data in CloudWatch logs could allow an attacker with read-only access to escalate their privileges or gain unauthorised access to systems.",
"RelatedUrl": "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudwatch-alarms-for-cloudtrail.html",
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "It is recommended that sensitive information is not logged to CloudWatch logs. Alternatively, sensitive data may be masked using a protection policy",
"Url": "https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/mask-sensitive-log-data.html"
}
},
"Categories": [
"secrets"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -1,147 +0,0 @@
from json import dumps
from typing import Set
from presidio_analyzer import AnalyzerEngine
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.cloudwatch.cloudwatch_service import (
convert_to_cloudwatch_timestamp_format,
)
from prowler.providers.aws.services.cloudwatch.logs_client import logs_client
class cloudwatch_log_group_no_critical_pii_in_logs(Check):
def execute(self):
findings = []
# Initialize the PII Analyzer engine
analyzer = AnalyzerEngine()
if logs_client.log_groups:
critical_pii_entities = logs_client.audit_config.get(
"critical_pii_entities",
[
"CREDIT_CARD",
"EMAIL_ADDRESS",
"PHONE_NUMBER",
"US_SSN",
"US_BANK_NUMBER",
"IBAN_CODE",
"US_PASSPORT",
],
)
pii_language = logs_client.audit_config.get("pii_language", "en")
for log_group in logs_client.log_groups.values():
report = Check_Report_AWS(self.metadata())
report.status = "PASS"
report.status_extended = (
f"No critical PII found in {log_group.name} log group."
)
report.region = log_group.region
report.resource_id = log_group.name
report.resource_arn = log_group.arn
report.resource_tags = log_group.tags
log_group_pii = []
if log_group.log_streams:
for log_stream_name in log_group.log_streams:
log_stream_pii = {}
log_stream_events = [
dumps(event["message"])
for event in log_group.log_streams[log_stream_name]
]
# Process log data in manageable chunks since the limit of Presidio Analyzer is 100,000 characters
MAX_CHUNK_SIZE = 100000
for i in range(0, len(log_stream_events)):
chunk = log_stream_events[i]
# Split if chunk exceeds max allowed size for analyzer
if len(chunk) > MAX_CHUNK_SIZE:
split_chunks = [
chunk[j : j + MAX_CHUNK_SIZE]
for j in range(0, len(chunk), MAX_CHUNK_SIZE)
]
else:
split_chunks = [chunk]
for split_chunk in split_chunks:
# PII detection for each split chunk
pii_detection_result = analyzer.analyze(
text=split_chunk,
entities=critical_pii_entities,
score_threshold=1,
language=pii_language,
)
# Track cumulative character count to map PII to log event
cumulative_char_count = 0
for j, log_event in enumerate(
log_stream_events[i : i + len(split_chunks)]
):
log_event_length = len(log_event)
for pii in pii_detection_result:
# Check if PII start position falls within this log event
if (
cumulative_char_count
<= pii.start
< cumulative_char_count + log_event_length
):
flagged_event = log_group.log_streams[
log_stream_name
][j]
cloudwatch_timestamp = (
convert_to_cloudwatch_timestamp_format(
flagged_event["timestamp"]
)
)
if (
cloudwatch_timestamp
not in log_stream_pii
):
log_stream_pii[cloudwatch_timestamp] = (
SecretsDict()
)
# Add the detected PII entity to log_stream_pii
log_stream_pii[
cloudwatch_timestamp
].add_secret(
pii.start - cumulative_char_count,
pii.entity_type,
)
cumulative_char_count += (
log_event_length + 1
) # +1 to account for '\n'
if log_stream_pii:
pii_string = "; ".join(
[
f"at {timestamp} - {str(log_stream_pii[timestamp])}"
for timestamp in log_stream_pii
]
)
log_group_pii.append(
f"in log stream {log_stream_name} {pii_string}"
)
if log_group_pii:
pii_string = "; ".join(log_group_pii)
report.status = "FAIL"
report.status_extended = f"Potential critical PII found in log group {log_group.name} {pii_string}."
findings.append(report)
return findings
class SecretsDict(dict[int, Set[str]]):
"""Dictionary to track unique PII types on each line."""
def add_secret(self, line_number: int, pii_type: str) -> None:
"""Add a PII type to a specific line number, ensuring no duplicates."""
self.setdefault(line_number, set()).add(pii_type)
def __str__(self) -> str:
"""Generate a formatted string representation of the dictionary."""
return ", ".join(
f"{', '.join(sorted(pii_types))} on line {line_number}"
for line_number, pii_types in sorted(self.items())
)
@@ -93,8 +93,6 @@ class Logs(AWSService):
if (
"cloudwatch_log_group_no_secrets_in_logs"
in provider.audit_metadata.expected_checks
or "cloudwatch_log_group_no_critical_pii_in_logs"
in provider.audit_metadata.expected_checks
):
self.events_per_log_group_threshold = (
1000 # The threshold for number of events to return per log group.
@@ -23,7 +23,9 @@
"Url": "https://docs.aws.amazon.com/directoryservice/latest/admin-guide/incident-response.html"
}
},
"Categories": [],
"Categories": [
"logging"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -12,11 +12,12 @@ class ec2_ami_public(Check):
report.resource_arn = image.arn
report.resource_tags = image.tags
report.status = "PASS"
report.status_extended = f"EC2 AMI {image.id} is not public."
report.status_extended = (
f"EC2 AMI {image.name if image.name else image.id} is not public."
)
if image.public:
report.status = "FAIL"
report.status_extended = f"EC2 AMI {image.id} is currently public."
report.resource_id = image.id
report.status_extended = f"EC2 AMI {image.name if image.name else image.id} is currently public."
findings.append(report)
@@ -357,7 +357,7 @@ class EC2(AWSService):
Image(
id=image["ImageId"],
arn=arn,
name=image["Name"],
name=image.get("Name", ""),
public=image.get("Public", False),
region=regional_client.region,
tags=image.get("Tags"),
@@ -26,7 +26,8 @@
}
},
"Categories": [
"forensics-ready"
"forensics-ready",
"logging"
],
"DependsOn": [],
"RelatedTo": [],
@@ -26,7 +26,8 @@
}
},
"Categories": [
"forensics-ready"
"forensics-ready",
"logging"
],
"DependsOn": [],
"RelatedTo": [],
@@ -25,7 +25,10 @@
"Url": "https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-access-logs.html"
}
},
"Categories": [],
"Categories": [
"forensics-ready",
"logging"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -0,0 +1,4 @@
from prowler.providers.aws.services.firehose.firehose_service import Firehose
from prowler.providers.common.provider import Provider
firehose_client = Firehose(Provider.get_global_provider())
@@ -0,0 +1,96 @@
from enum import Enum
from typing import Dict, List, Optional
from botocore.client import ClientError
from pydantic import BaseModel, Field
from prowler.lib.logger import logger
from prowler.lib.scan_filters.scan_filters import is_resource_filtered
from prowler.providers.aws.lib.service.service import AWSService
class Firehose(AWSService):
def __init__(self, provider):
# Call AWSService's __init__
super().__init__(__class__.__name__, provider)
self.delivery_streams = {}
self.__threading_call__(self._list_delivery_streams)
self.__threading_call__(
self._list_tags_for_delivery_stream, self.delivery_streams.values()
)
self.__threading_call__(
self._describe_delivery_stream, self.delivery_streams.values()
)
def _list_delivery_streams(self, regional_client):
logger.info("Firehose - Listing delivery streams...")
try:
for stream_name in regional_client.list_delivery_streams()[
"DeliveryStreamNames"
]:
stream_arn = f"arn:{self.audited_partition}:firehose:{regional_client.region}:{self.audited_account}:deliverystream/{stream_name}"
if not self.audit_resources or (
is_resource_filtered(stream_arn, self.audit_resources)
):
self.delivery_streams[stream_arn] = DeliveryStream(
arn=stream_arn,
name=stream_name,
region=regional_client.region,
)
except ClientError as error:
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _list_tags_for_delivery_stream(self, stream):
logger.info(f"Firehose - Listing tags for stream {stream.name}...")
try:
stream.tags = (
self.regional_clients[stream.region]
.list_tags_for_delivery_stream(DeliveryStreamName=stream.name)
.get("Tags", [])
)
except ClientError as error:
logger.error(
f"{stream.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _describe_delivery_stream(self, stream):
logger.info(f"Firehose - Describing stream {stream.name}...")
try:
describe_stream = self.regional_clients[
stream.region
].describe_delivery_stream(DeliveryStreamName=stream.name)
encryption_config = describe_stream.get(
"DeliveryStreamDescription", {}
).get("DeliveryStreamEncryptionConfiguration", {})
stream.kms_encryption = EncryptionStatus(
encryption_config.get("Status", "DISABLED")
)
stream.kms_key_arn = encryption_config.get("KeyARN", "")
except ClientError as error:
logger.error(
f"{stream.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
class EncryptionStatus(Enum):
"""Possible values for the status of the encryption of a Firehose stream"""
ENABLED = "ENABLED"
DISABLED = "DISABLED"
ENABLING = "ENABLING"
DISABLING = "DISABLING"
ENABLING_FAILED = "ENABLING_FAILED"
DISABLING_FAILED = "DISABLING_FAILED"
class DeliveryStream(BaseModel):
"""Model for a Firehose Delivery Stream"""
arn: str
name: str
region: str
kms_key_arn: Optional[str] = Field(default_factory=str)
kms_encryption: Optional[str] = Field(default_factory=str)
tags: Optional[List[Dict[str, str]]] = Field(default_factory=list)
@@ -0,0 +1,32 @@
{
"Provider": "aws",
"CheckID": "firehose_stream_encrypted_at_rest",
"CheckTitle": "DataFirehose delivery streams should be encrypted at rest.",
"CheckType": [
"Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls"
],
"ServiceName": "firehose",
"SubServiceName": "Ensure DataFirehose delivery streams are encrypted at rest.",
"ResourceIdTemplate": "arn:partition:firehose:region:account-id:deliverystream/delivery-stream-id",
"Severity": "medium",
"ResourceType": "AwsKinesisFirehoseDeliveryStream",
"Description": "",
"Risk": "Without encryption at rest, data in Amazon Kinesis Data Firehose delivery streams is vulnerable to unauthorized access if the storage layer is compromised. This increases the risk of sensitive information exposure, potentially leading to data breaches or non-compliance with security regulations.",
"RelatedUrl": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html",
"Remediation": {
"Code": {
"CLI": "aws firehose update-delivery-stream --delivery-stream-name <delivery-stream-name> --delivery-stream-encryption-configuration-input '{ \"KeyType\": \"CUSTOMER_MANAGED_CMK\", \"KeyARN\": \"<kms-key-arn>\" }'",
"NativeIaC": "https://docs.prowler.com/checks/aws/general-policies/ensure-aws-kinesis-firehoses-delivery-stream-is-encrypted/",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/datafirehose-controls.html#datafirehose-1",
"Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Firehose/delivery-stream-encrypted-with-kms-customer-master-keys.html"
},
"Recommendation": {
"Text": "Enable server-side encryption for Kinesis Firehose delivery streams using AWS Key Management Service (KMS). This encrypts data at rest, ensuring that sensitive information remains secure and compliant with regulatory standards.",
"Url": "https://docs.aws.amazon.com/firehose/latest/dev/encryption.html"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,43 @@
from typing import List
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.firehose.firehose_client import firehose_client
from prowler.providers.aws.services.firehose.firehose_service import EncryptionStatus
class firehose_stream_encrypted_at_rest(Check):
"""Check if Firehose Streams are encrypted at rest.
This class verifies that all Firehose Streams have at rest encryption enabled by checking if KMS encryption is active and a KMS Key is configured.
"""
def execute(self) -> List[Check_Report_AWS]:
"""Execute the Firehose Stream Encrypted at Rest check.
Iterates over all Firehose Streams and checks if KMS encryption is enabled and a KMS Key is configured.
Returns:
List[Check_Report_AWS]: A list of reports for each Firehose Stream.
"""
findings = []
for stream in firehose_client.delivery_streams.values():
report = Check_Report_AWS(self.metadata())
report.region = stream.region
report.resource_id = stream.name
report.resource_arn = stream.arn
report.resource_tags = stream.tags
report.status = "PASS"
report.status_extended = (
f"Firehose Stream {stream.name} does have at rest encryption enabled."
)
if (
stream.kms_encryption != EncryptionStatus.ENABLED
or not stream.kms_key_arn
):
report.status = "FAIL"
report.status_extended = f"Firehose Stream {stream.name} does not have at rest encryption enabled."
findings.append(report)
return findings
@@ -25,7 +25,9 @@
"Url": "https://docs.aws.amazon.com/glue/latest/dg/monitor-continuous-logging-enable.html"
}
},
"Categories": [],
"Categories": [
"logging"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "This check is being removed since logs for all AWS Glue jobs are now always sent to Amazon CloudWatch."
@@ -5,35 +5,39 @@ from prowler.providers.aws.services.iam.iam_client import iam_client
class iam_no_root_access_key(Check):
def execute(self) -> Check_Report_AWS:
findings = []
response = iam_client.credential_report
for user in response:
if user["user"] == "<root_account>":
report = Check_Report_AWS(self.metadata())
report.region = iam_client.region
report.resource_id = user["user"]
report.resource_arn = user["arn"]
if (
user["access_key_1_active"] == "false"
and user["access_key_2_active"] == "false"
):
report.status = "PASS"
report.status_extended = (
f"User {user['user']} does not have access keys."
)
elif (
user["access_key_1_active"] == "true"
and user["access_key_2_active"] == "true"
):
report.status = "FAIL"
report.status_extended = (
f"User {user['user']} has two active access keys."
)
else:
report.status = "FAIL"
report.status_extended = (
f"User {user['user']} has one active access key."
)
findings.append(report)
# Check if the root credentials are managed by AWS Organizations
if (
iam_client.organization_features is not None
and "RootCredentialsManagement" not in iam_client.organization_features
):
for user in iam_client.credential_report:
if user["user"] == "<root_account>":
report = Check_Report_AWS(self.metadata())
report.region = iam_client.region
report.resource_id = user["user"]
report.resource_arn = user["arn"]
if (
user["access_key_1_active"] == "false"
and user["access_key_2_active"] == "false"
):
report.status = "PASS"
report.status_extended = (
"Root account does not have access keys."
)
elif (
user["access_key_1_active"] == "true"
and user["access_key_2_active"] == "true"
):
report.status = "FAIL"
report.status_extended = (
"Root account has two active access keys."
)
else:
report.status = "FAIL"
report.status_extended = (
"Root account has one active access key."
)
findings.append(report)
break
return findings
@@ -28,5 +28,5 @@
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "CAF Security Epic: IAM"
"Notes": ""
}
@@ -0,0 +1,34 @@
{
"Provider": "aws",
"CheckID": "iam_root_credentials_management_enabled",
"CheckTitle": "Ensure centralized root credentials management is enabled",
"CheckType": [],
"ServiceName": "iam",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
"Severity": "high",
"ResourceType": "Other",
"Description": "Checks if centralized management of root credentials for member accounts in AWS Organizations is enabled. This ensures that root credentials are managed centrally, reducing the risk of unauthorized access or mismanagement.",
"Risk": "Without centralized root credentials management, member accounts retain full control over their root user credentials, increasing the risk of credential misuse, mismanagement, or compromise.",
"RelatedUrl": "https://docs.aws.amazon.com/IAM/latest/UserGuide/id_root-user.html#id_root-user-access-management",
"Remediation": {
"Code": {
"CLI": "aws iam enable-organizations-root-credentials-management",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable centralized management of root access for member accounts using the CLI or IAM console.",
"Url": "https://docs.aws.amazon.com/IAM/latest/UserGuide/id_root-enable-root-access.html"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [
"iam_root_hardware_mfa_enabled",
"iam_root_mfa_enabled",
"iam_no_root_access_key"
],
"Notes": "This check skips findings for member accounts as they cannot execute the ListOrganizationsFeatures API call, which is restricted to the management account or delegated administrators."
}
@@ -0,0 +1,28 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.iam.iam_client import iam_client
from prowler.providers.aws.services.organizations.organizations_client import (
organizations_client,
)
class iam_root_credentials_management_enabled(Check):
def execute(self) -> Check_Report_AWS:
findings = []
if (
organizations_client.organization
and organizations_client.organization.status == "ACTIVE"
and iam_client.organization_features is not None
):
report = Check_Report_AWS(self.metadata())
report.region = iam_client.region
report.resource_arn = iam_client.audited_account_arn
report.resource_id = iam_client.audited_account
if "RootCredentialsManagement" in iam_client.organization_features:
report.status = "PASS"
report.status_extended = "Root credentials management is enabled."
else:
report.status = "FAIL"
report.status_extended = "Root credentials management is not enabled."
findings.append(report)
return findings
@@ -5,31 +5,39 @@ from prowler.providers.aws.services.iam.iam_client import iam_client
class iam_root_hardware_mfa_enabled(Check):
def execute(self) -> Check_Report_AWS:
findings = []
# This check is only avaible in Commercial Partition
# This check is only available in Commercial Partition
if iam_client.audited_partition == "aws":
if iam_client.account_summary:
virtual_mfa = False
report = Check_Report_AWS(self.metadata())
report.region = iam_client.region
report.resource_id = "<root_account>"
report.resource_arn = iam_client.mfa_arn_template
# Check if the root credentials are managed by AWS Organizations
if (
iam_client.organization_features is not None
and "RootCredentialsManagement" not in iam_client.organization_features
):
if iam_client.account_summary:
virtual_mfa = False
report = Check_Report_AWS(self.metadata())
report.region = iam_client.region
report.resource_id = "<root_account>"
report.resource_arn = iam_client.mfa_arn_template
if iam_client.account_summary["SummaryMap"]["AccountMFAEnabled"] > 0:
for mfa in iam_client.virtual_mfa_devices:
# If the ARN of the associated IAM user of the Virtual MFA device is "arn:aws:iam::[aws-account-id]:root", your AWS root account is not using a hardware-based MFA device for MFA protection.
if "root" in mfa.get("User", {}).get("Arn", ""):
virtual_mfa = True
report.status = "FAIL"
report.status_extended = "Root account has a virtual MFA instead of a hardware MFA device enabled."
if not virtual_mfa:
report.status = "PASS"
report.status_extended = (
"Root account has a hardware MFA device enabled."
)
else:
report.status = "FAIL"
report.status_extended = "MFA is not enabled for root account."
if (
iam_client.account_summary["SummaryMap"]["AccountMFAEnabled"]
> 0
):
for mfa in iam_client.virtual_mfa_devices:
# If the ARN of the associated IAM user of the Virtual MFA device is "arn:aws:iam::[aws-account-id]:root", your AWS root account is not using a hardware-based MFA device for MFA protection.
if "root" in mfa.get("User", {}).get("Arn", ""):
virtual_mfa = True
report.status = "FAIL"
report.status_extended = "Root account has a virtual MFA instead of a hardware MFA device enabled."
if not virtual_mfa:
report.status = "PASS"
report.status_extended = (
"Root account has a hardware MFA device enabled."
)
else:
report.status = "FAIL"
report.status_extended = "MFA is not enabled for root account."
findings.append(report)
findings.append(report)
return findings
@@ -5,19 +5,26 @@ from prowler.providers.aws.services.iam.iam_client import iam_client
class iam_root_mfa_enabled(Check):
def execute(self) -> Check_Report_AWS:
findings = []
if iam_client.credential_report:
for user in iam_client.credential_report:
if user["user"] == "<root_account>":
report = Check_Report_AWS(self.metadata())
report.region = iam_client.region
report.resource_id = user["user"]
report.resource_arn = user["arn"]
if user["mfa_active"] == "false":
report.status = "FAIL"
report.status_extended = "MFA is not enabled for root account."
else:
report.status = "PASS"
report.status_extended = "MFA is enabled for root account."
findings.append(report)
# Check if the root credentials are managed by AWS Organizations
if (
iam_client.organization_features is not None
and "RootCredentialsManagement" not in iam_client.organization_features
):
if iam_client.credential_report:
for user in iam_client.credential_report:
if user["user"] == "<root_account>":
report = Check_Report_AWS(self.metadata())
report.region = iam_client.region
report.resource_id = user["user"]
report.resource_arn = user["arn"]
if user["mfa_active"] == "false":
report.status = "FAIL"
report.status_extended = (
"MFA is not enabled for root account."
)
else:
report.status = "PASS"
report.status_extended = "MFA is enabled for root account."
findings.append(report)
return findings
@@ -102,6 +102,8 @@ class IAM(AWSService):
self._get_last_accessed_services()
self.user_temporary_credentials_usage = {}
self._get_user_temporary_credentials_usage()
self.organization_features = []
self._list_organizations_features()
# List missing tags
self.__threading_call__(self._list_tags, self.users)
self.__threading_call__(self._list_tags, self.roles)
@@ -964,6 +966,46 @@ class IAM(AWSService):
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _list_organizations_features(self):
logger.info("IAM - List Organization Features...")
try:
organization_features = self.client.list_organizations_features()
self.organization_features = organization_features.get(
"EnabledFeatures", []
)
except ClientError as error:
if error.response["Error"]["Code"] == "OrganizationNotFoundException":
logger.warning(
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
elif error.response["Error"]["Code"] == "ServiceAccessNotEnabledException":
logger.warning(
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
elif (
error.response["Error"]["Code"]
== "OrganizationNotInAllFeaturesModeException"
):
logger.warning(
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
elif (
error.response["Error"]["Code"]
== "AccountNotManagementOrDelegatedAdministratorException"
):
logger.warning(
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
self.organization_features = None
else:
logger.error(
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except Exception as error:
logger.error(
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
class MFADevice(BaseModel):
serial_number: str
@@ -168,17 +168,22 @@ def is_policy_public(
in principal.get("CanonicalUser", "")
or check_cross_service_confused_deputy
and (
( # Check if function can be invoked by other AWS services if check_cross_service_confused_deputy is True
(
".amazonaws.com" in principal.get("Service", "")
or ".amazon.com" in principal.get("Service", "")
or "*" in principal.get("Service", "")
)
# Check if function can be invoked by other AWS services if check_cross_service_confused_deputy is True
(
".amazonaws.com" in principal.get("Service", "")
or ".amazon.com" in principal.get("Service", "")
or "*" in principal.get("Service", "")
)
and (
"secretsmanager.amazonaws.com"
not in principal.get(
"Service", ""
) # AWS ensures that resources called by SecretsManager are executed in the same AWS account
or "eks.amazonaws.com"
not in principal.get(
"Service", ""
) # AWS ensures that resources called by EKS are executed in the same AWS account
)
and "secretsmanager.amazonaws.com"
not in principal.get(
"Service", ""
) # AWS ensures that resources called by SecretsManager are executed in the same AWS account
)
)
)
@@ -1,7 +1,7 @@
from enum import Enum
from typing import Optional
from typing import Dict, List, Optional
from pydantic import BaseModel
from pydantic import BaseModel, Field
from prowler.lib.logger import logger
from prowler.lib.scan_filters.scan_filters import is_resource_filtered
@@ -49,6 +49,7 @@ class Kinesis(AWSService):
stream.encrypted_at_rest = EncryptionType(
stream_description.get("EncryptionType", "NONE")
)
stream.retention_period = stream_description.get("RetentionPeriodHours", 24)
except Exception as error:
logger.error(
f"{stream.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
@@ -91,5 +92,6 @@ class Stream(BaseModel):
region: str
name: str
status: StreamStatus
tags: Optional[list]
tags: Optional[List[Dict[str, str]]] = Field(default_factory=list)
encrypted_at_rest: EncryptionType = EncryptionType.NONE
retention_period: int = 24 # 1 day
@@ -0,0 +1,32 @@
{
"Provider": "aws",
"CheckID": "kinesis_stream_data_retention_period",
"CheckTitle": "Kinesis streams should have an adequate data retention period.",
"CheckType": [
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices"
],
"ServiceName": "kinesis",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:kinesis::account-id:stream/stream-name",
"Severity": "medium",
"ResourceType": "AwsKinesisStream",
"Description": "Ensure Kinesis streams have an adequate data retention period.",
"Risk": "An inadequate data retention period may result in data records being deleted before they can be processed or backed up, increasing the risk of data loss. This is especially critical for applications that rely on historical data availability for analysis, monitoring, and recovery in case of failures.",
"RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/kinesis-stream-backup-retention-check.html",
"Remediation": {
"Code": {
"CLI": "aws kinesis increase-stream-retention-period --stream-name <stream-name> --retention-period-hours <hours>",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/kinesis-controls.html#kinesis-3",
"Terraform": ""
},
"Recommendation": {
"Text": "Configure an adequate data retention period for Kinesis streams to ensure data is available for the required timeframe. Set the retention period based on your application’s data retention requirements, and consider at least 168 hours (or customize as necessary).",
"Url": "https://docs.aws.amazon.com/streams/latest/dev/kinesis-extended-retention.html"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,37 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.kinesis.kinesis_client import kinesis_client
class kinesis_stream_data_retention_period(Check):
"""Ensure Kinesis Stream has an adequate data retention period
The retention period for Kinesis Streams should be set to a value that meets the organization's data retention policy.
"""
def execute(self):
"""Execute Check Kinesis Stream data retention period
Iterate over all Kinesis Streams and check if the retention period is adequate.
Returns:
findings (list): List of findings
"""
findings = []
for stream in kinesis_client.streams.values():
report = Check_Report_AWS(self.metadata())
report.region = stream.region
report.resource_id = stream.name
report.resource_arn = stream.arn
report.resource_tags = stream.tags
report.status = "FAIL"
report.status_extended = f"Kinesis Stream {stream.name} does not have an adequate data retention period ({stream.retention_period}hrs)."
if stream.retention_period >= kinesis_client.audit_config.get(
"min_kinesis_stream_retention_hours", 168
):
report.status = "PASS"
report.status_extended = f"Kinesis Stream {stream.name} does have an adequate data retention period ({stream.retention_period}hrs)."
findings.append(report)
return findings
@@ -25,7 +25,9 @@
"Url": "https://docs.aws.amazon.com/amazon-mq/latest/developer-guide/configure-logging-monitoring-activemq.html"
}
},
"Categories": [],
"Categories": [
"logging"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -11,7 +11,7 @@ from prowler.providers.aws.lib.service.service import AWSService
class MQ(AWSService):
def __init__(self, provider):
# Call AWSService's __init__
super().__init__("mq", provider)
super().__init__(__class__.__name__, provider)
self.brokers = {}
self.__threading_call__(self._list_brokers)
self.__threading_call__(self._describe_broker, self.brokers.values())
@@ -25,7 +25,9 @@
"Url": "https://docs.aws.amazon.com/neptune/latest/userguide/cloudwatch-logs.html"
}
},
"Categories": [],
"Categories": [
"logging"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -27,7 +27,8 @@
}
},
"Categories": [
"forensics-ready"
"forensics-ready",
"logging"
],
"DependsOn": [],
"RelatedTo": [],
@@ -27,7 +27,8 @@
}
},
"Categories": [
"forensics-ready"
"forensics-ready",
"logging"
],
"DependsOn": [],
"RelatedTo": [],
@@ -7,21 +7,19 @@ from prowler.providers.aws.services.organizations.organizations_client import (
class organizations_account_part_of_organizations(Check):
def execute(self):
findings = []
for org in organizations_client.organizations:
if organizations_client.organization:
report = Check_Report_AWS(self.metadata())
if org.status == "ACTIVE":
if organizations_client.organization.status == "ACTIVE":
report.status = "PASS"
report.status_extended = (
f"AWS Organization {org.id} contains this AWS account."
)
report.status_extended = f"AWS Organization {organizations_client.organization.id} contains this AWS account."
else:
report.status = "FAIL"
report.status_extended = (
"AWS Organizations is not in-use for this AWS Account."
)
report.region = organizations_client.region
report.resource_id = org.id
report.resource_arn = org.arn
report.resource_id = organizations_client.organization.id
report.resource_arn = organizations_client.organization.arn
findings.append(report)
return findings
@@ -14,31 +14,33 @@ class organizations_delegated_administrators(Check):
)
)
for org in organizations_client.organizations:
if org.status == "ACTIVE":
report = Check_Report_AWS(self.metadata())
report.resource_id = org.id
report.resource_arn = org.arn
report.region = organizations_client.region
if org.delegated_administrators is None:
# Access Denied to list_policies
continue
if org.delegated_administrators:
for delegated_administrator in org.delegated_administrators:
if (
organizations_client.organization
and organizations_client.organization.status == "ACTIVE"
):
report = Check_Report_AWS(self.metadata())
report.resource_id = organizations_client.organization.id
report.resource_arn = organizations_client.organization.arn
report.region = organizations_client.region
if (
organizations_client.organization.delegated_administrators is not None
): # Check if Access Denied to list_delegated_administrators
if organizations_client.organization.delegated_administrators:
for (
delegated_administrator
) in organizations_client.organization.delegated_administrators:
if (
delegated_administrator.id
not in organizations_trusted_delegated_administrators
):
report.status = "FAIL"
report.status_extended = f"AWS Organization {org.id} has an untrusted Delegated Administrator: {delegated_administrator.id}."
report.status_extended = f"AWS Organization {organizations_client.organization.id} has an untrusted Delegated Administrator: {delegated_administrator.id}."
else:
report.status = "PASS"
report.status_extended = f"AWS Organization {org.id} has a trusted Delegated Administrator: {delegated_administrator.id}."
report.status_extended = f"AWS Organization {organizations_client.organization.id} has a trusted Delegated Administrator: {delegated_administrator.id}."
else:
report.status = "PASS"
report.status_extended = (
f"AWS Organization {org.id} has no Delegated Administrators."
)
report.status_extended = f"AWS Organization {organizations_client.organization.id} has no Delegated Administrators."
findings.append(report)
@@ -8,19 +8,23 @@ class organizations_opt_out_ai_services_policy(Check):
def execute(self):
findings = []
for org in organizations_client.organizations:
if org.policies is not None: # Access Denied to list_policies
if organizations_client.organization:
if (
organizations_client.organization.policies is not None
): # Access Denied to list_policies
report = Check_Report_AWS(self.metadata())
report.resource_id = org.id
report.resource_arn = org.arn
report.resource_id = organizations_client.organization.id
report.resource_arn = organizations_client.organization.arn
report.region = organizations_client.region
report.status = "FAIL"
report.status_extended = (
"AWS Organizations is not in-use for this AWS Account."
)
if org.status == "ACTIVE":
report.status_extended = f"AWS Organization {org.id} has not opted out of all AI services, granting consent for AWS to access its data."
for policy in org.policies.get("AISERVICES_OPT_OUT_POLICY", []):
if organizations_client.organization.status == "ACTIVE":
report.status_extended = f"AWS Organization {organizations_client.organization.id} has not opted out of all AI services, granting consent for AWS to access its data."
for policy in organizations_client.organization.policies.get(
"AISERVICES_OPT_OUT_POLICY", []
):
if (
policy.content.get("services", {})
.get("default", {})
@@ -29,7 +33,7 @@ class organizations_opt_out_ai_services_policy(Check):
== "optOut"
):
report.status = "PASS"
report.status_extended = f"AWS Organization {org.id} has opted out of all AI services, not granting consent for AWS to access its data."
report.status_extended = f"AWS Organization {organizations_client.organization.id} has opted out of all AI services, not granting consent for AWS to access its data."
break
findings.append(report)
@@ -11,25 +11,27 @@ class organizations_scp_check_deny_regions(Check):
"organizations_enabled_regions", []
)
for org in organizations_client.organizations:
if org.policies is not None: # Access denied to list policies
if organizations_client.organization:
if (
organizations_client.organization.policies is not None
): # Access denied to list policies
report = Check_Report_AWS(self.metadata())
report.resource_id = org.id
report.resource_arn = org.arn
report.resource_id = organizations_client.organization.id
report.resource_arn = organizations_client.organization.arn
report.region = organizations_client.region
report.status = "FAIL"
report.status_extended = (
"AWS Organizations is not in-use for this AWS Account."
)
if org.status == "ACTIVE":
report.status_extended = (
f"AWS Organizations {org.id} does not have SCP policies."
)
if organizations_client.organization.status == "ACTIVE":
report.status_extended = f"AWS Organizations {organizations_client.organization.id} does not have SCP policies."
# We use this flag if we find a statement that is restricting regions but not all the configured ones:
is_region_restricted_statement = False
for policy in org.policies.get("SERVICE_CONTROL_POLICY", []):
for policy in organizations_client.organization.policies.get(
"SERVICE_CONTROL_POLICY", []
):
# Statements are not always list
statements = policy.content.get("Statement")
@@ -54,14 +56,14 @@ class organizations_scp_check_deny_regions(Check):
):
# All defined regions are restricted, we exit here, no need to continue.
report.status = "PASS"
report.status_extended = f"AWS Organization {org.id} has SCP policy {policy.id} restricting all configured regions found."
report.status_extended = f"AWS Organization {organizations_client.organization.id} has SCP policy {policy.id} restricting all configured regions found."
findings.append(report)
return findings
else:
# Regions are restricted, but not the ones defined, we keep this finding, but we continue analyzing:
is_region_restricted_statement = True
report.status = "FAIL"
report.status_extended = f"AWS Organization {org.id} has SCP policies {policy.id} restricting some AWS Regions, but not all the configured ones, please check config."
report.status_extended = f"AWS Organization {organizations_client.organization.id} has SCP policies {policy.id} restricting some AWS Regions, but not all the configured ones, please check config."
# Allow if Condition = {"StringEquals": {"aws:RequestedRegion": [region1, region2]}}
if (
@@ -80,18 +82,18 @@ class organizations_scp_check_deny_regions(Check):
):
# All defined regions are restricted, we exit here, no need to continue.
report.status = "PASS"
report.status_extended = f"AWS Organization {org.id} has SCP policy {policy.id} restricting all configured regions found."
report.status_extended = f"AWS Organization {organizations_client.organization.id} has SCP policy {policy.id} restricting all configured regions found."
findings.append(report)
return findings
else:
# Regions are restricted, but not the ones defined, we keep this finding, but we continue analyzing:
is_region_restricted_statement = True
report.status = "FAIL"
report.status_extended = f"AWS Organization {org.id} has SCP policies {policy.id} restricting some AWS Regions, but not all the configured ones, please check config."
report.status_extended = f"AWS Organization {organizations_client.organization.id} has SCP policies {policy.id} restricting some AWS Regions, but not all the configured ones, please check config."
if not is_region_restricted_statement:
report.status = "FAIL"
report.status_extended = f"AWS Organization {org.id} has SCP policies but don't restrict AWS Regions."
report.status_extended = f"AWS Organization {organizations_client.organization.id} has SCP policies but don't restrict AWS Regions."
findings.append(report)
@@ -20,7 +20,7 @@ class Organizations(AWSService):
def __init__(self, provider):
# Call AWSService's __init__
super().__init__(__class__.__name__, provider)
self.organizations = []
self.organization = None
self.policies = {}
self.delegated_administrators = []
self._describe_organization()
@@ -43,13 +43,11 @@ class Organizations(AWSService):
error.response["Error"]["Code"]
== "AWSOrganizationsNotInUseException"
):
self.organizations.append(
Organization(
arn=self.audited_account_arn,
id="AWS Organization",
status="NOT_AVAILABLE",
master_id="",
)
self.organization = Organization(
arn=self.audited_account_arn,
id="AWS Organization",
status="NOT_AVAILABLE",
master_id="",
)
else:
logger.error(
@@ -59,25 +57,20 @@ class Organizations(AWSService):
if not self.audit_resources or (
is_resource_filtered(organization_arn, self.audit_resources)
):
self.organizations.append(
Organization(
arn=organization_arn,
id=organization_id,
status="ACTIVE",
master_id=organization_master_id,
policies=organization_policies,
delegated_administrators=organization_delegated_administrator,
)
self.organization = Organization(
arn=organization_arn,
id=organization_id,
status="ACTIVE",
master_id=organization_master_id,
policies=organization_policies,
delegated_administrators=organization_delegated_administrator,
)
else:
# is filtered
self.organizations.append(
Organization(
arn=self.audited_account_arn,
id="AWS Organization",
status="NOT_AVAILABLE",
master_id="",
)
self.organization = Organization(
arn=self.audited_account_arn,
id="AWS Organization",
status="NOT_AVAILABLE",
master_id="",
)
except Exception as error:
@@ -8,26 +8,28 @@ class organizations_tags_policies_enabled_and_attached(Check):
def execute(self):
findings = []
for org in organizations_client.organizations:
if org.policies is not None: # Access Denied to list_policies
if organizations_client.organization:
if (
organizations_client.organization.policies is not None
): # Access Denied to list_policies
report = Check_Report_AWS(self.metadata())
report.resource_id = org.id
report.resource_arn = org.arn
report.resource_id = organizations_client.organization.id
report.resource_arn = organizations_client.organization.arn
report.region = organizations_client.region
report.status = "FAIL"
report.status_extended = (
"AWS Organizations is not in-use for this AWS Account."
)
if org.status == "ACTIVE":
report.status_extended = (
f"AWS Organizations {org.id} does not have tag policies."
)
for policy in org.policies.get("TAG_POLICY", []):
report.status_extended = f"AWS Organization {org.id} has tag policies enabled but not attached."
if organizations_client.organization.status == "ACTIVE":
report.status_extended = f"AWS Organizations {organizations_client.organization.id} does not have tag policies."
for policy in organizations_client.organization.policies.get(
"TAG_POLICY", []
):
report.status_extended = f"AWS Organization {organizations_client.organization.id} has tag policies enabled but not attached."
if policy.targets:
report.status = "PASS"
report.status_extended = f"AWS Organization {org.id} has tag policies enabled and attached to an AWS account."
report.status_extended = f"AWS Organization {organizations_client.organization.id} has tag policies enabled and attached to an AWS account."
findings.append(report)
@@ -23,7 +23,9 @@
"Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/publishing_cloudwatchlogs.html"
}
},
"Categories": [],
"Categories": [
"logging"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -23,7 +23,9 @@
"Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/publishing_cloudwatchlogs.html"
}
},
"Categories": [],
"Categories": [
"logging"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -0,0 +1,63 @@
from prowler.lib.logger import logger
from prowler.providers.aws.services.rds.rds_client import rds_client
def fixer(resource_id: str, region: str) -> bool:
"""
Modify the attributes of an RDS DB snapshot or DB cluster snapshot to remove public access.
Specifically, this fixer removes the 'all' value from the 'restore' attribute to
prevent the snapshot from being publicly accessible for both DB snapshots and DB cluster snapshots.
Requires the rds:ModifyDBSnapshotAttribute or rds:ModifyDBClusterSnapshotAttribute permissions.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "rds:ModifyDBSnapshotAttribute",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "rds:ModifyDBClusterSnapshotAttribute",
"Resource": "*"
}
]
}
Args:
resource_id (str): The DB snapshot or DB cluster snapshot identifier.
region (str): AWS region where the snapshot exists.
Returns:
bool: True if the operation is successful (public access is removed), False otherwise.
"""
try:
regional_client = rds_client.regional_clients[region]
# Check if the resource is a DB Cluster Snapshot or a DB Instance Snapshot
try:
regional_client.describe_db_cluster_snapshots(
DBClusterSnapshotIdentifier=resource_id
)
# If the describe call is successful, it's a DB cluster snapshot
regional_client.modify_db_cluster_snapshot_attribute(
DBClusterSnapshotIdentifier=resource_id,
AttributeName="restore",
ValuesToRemove=["all"],
)
except regional_client.exceptions.DBClusterSnapshotNotFoundFault:
# If the DB cluster snapshot doesn't exist, it's an instance snapshot
regional_client.modify_db_snapshot_attribute(
DBSnapshotIdentifier=resource_id,
AttributeName="restore",
ValuesToRemove=["all"],
)
except Exception as error:
logger.error(
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return False
else:
return True
@@ -24,7 +24,8 @@
}
},
"Categories": [
"forensics-ready"
"forensics-ready",
"logging"
],
"DependsOn": [],
"RelatedTo": [],
@@ -10,23 +10,25 @@ from prowler.providers.aws.services.servicecatalog.servicecatalog_client import
class servicecatalog_portfolio_shared_within_organization_only(Check):
def execute(self):
findings = []
for org in organizations_client.organizations:
if org.status == "ACTIVE":
for portfolio in servicecatalog_client.portfolios.values():
if portfolio.shares is not None:
report = Check_Report_AWS(self.metadata())
report.region = portfolio.region
report.resource_id = portfolio.id
report.resource_arn = portfolio.arn
report.resource_tags = portfolio.tags
report.status = "PASS"
report.status_extended = f"ServiceCatalog Portfolio {portfolio.name} is shared within your AWS Organization."
for portfolio_share in portfolio.shares:
if portfolio_share.type == "ACCOUNT":
report.status = "FAIL"
report.status_extended = f"ServiceCatalog Portfolio {portfolio.name} is shared with an account."
break
if (
organizations_client.organization
and organizations_client.organization.status == "ACTIVE"
):
for portfolio in servicecatalog_client.portfolios.values():
if portfolio.shares is not None:
report = Check_Report_AWS(self.metadata())
report.region = portfolio.region
report.resource_id = portfolio.id
report.resource_arn = portfolio.arn
report.resource_tags = portfolio.tags
report.status = "PASS"
report.status_extended = f"ServiceCatalog Portfolio {portfolio.name} is shared within your AWS Organization."
for portfolio_share in portfolio.shares:
if portfolio_share.type == "ACCOUNT":
report.status = "FAIL"
report.status_extended = f"ServiceCatalog Portfolio {portfolio.name} is shared with an account."
break
findings.append(report)
findings.append(report)
return findings
@@ -26,7 +26,8 @@
}
},
"Categories": [
"forensics-ready"
"forensics-ready",
"logging"
],
"DependsOn": [],
"RelatedTo": [],
@@ -99,7 +99,7 @@ class WAFv2(AWSService):
def _list_resources_for_web_acl(self, acl):
logger.info("WAFv2 - Describing resources...")
try:
if acl.scope == Scope.REGIONAL or acl.region in self.regional_clients:
if acl.scope == Scope.REGIONAL:
for resource in self.regional_clients[
acl.region
].list_resources_for_web_acl(
+116 -21
View File
@@ -2,7 +2,9 @@ import asyncio
import os
import re
from argparse import ArgumentTypeError
from itertools import chain
from os import getenv
from typing import Union
from uuid import UUID
import requests
@@ -150,6 +152,68 @@ class AzureProvider(Provider):
AzureConfigCredentialsError: If there is an error in configuring the Azure credentials from a dictionary.
AzureGetTokenIdentityError: If there is an error in getting the token from the Azure identity.
AzureHTTPResponseError: If there is an HTTP response error.
Usage:
- Authentication: By default Prowler uses Azure Python SDK identity package authentication methods using the classes DefaultAzureCredential and InteractiveBrowserCredential.
- Using static credentials:
>>> AzureProvider(
... az_cli_auth=False,
... sp_env_auth=False,
... browser_auth=False,
... managed_identity_auth=False,
... tenant_id="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
... client_id="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
... client_secret="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
... )
- Using Azure CLI authentication:
>>> AzureProvider(
... az_cli_auth=True,
... sp_env_auth=False,
... browser_auth=False,
... managed_identity_auth=False,
... )
- Using Service Principal environment authentication:
>>> AzureProvider(
... az_cli_auth=False,
... sp_env_auth=True,
... browser_auth=False,
... managed_identity_auth=False,
... )
- Using interactive browser authentication:
>>> AzureProvider(
... az_cli_auth=False,
... sp_env_auth=False,
... browser_auth=True,
... managed_identity_auth=False,
... tenant_id="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
... )
* Note: Azure Tenant ID is required for browser authentication mode.
- Using managed identity authentication:
>>> AzureProvider(
... az_cli_auth=False,
... sp_env_auth=False,
... browser_auth=False,
... managed_identity_auth=True,
... )
- Non default azure region: Microsoft provides clouds for compliance with regional laws, which are available for your use. By default, Prowler uses AzureCloud cloud which is the comercial one.
If you want to use a different one, you can specify it using the region parameter.
>>> AzureProvider(
... az_cli_auth=False,
... sp_env_auth=True,
... browser_auth=False,
... managed_identity_auth=False,
... region="AzureUSGovernment",
... )
- Subscriptions: rowler is multisubscription, which means that is going to scan all the subscriptions is able to list. If you only assign permissions to one subscription, it is going to scan a single one.
Prowler also allows you to specify the subscriptions you want to scan by passing a list of subscription IDs.
>>> AzureProvider(
... az_cli_auth=False,
... sp_env_auth=True,
... browser_auth=False,
... managed_identity_auth=False,
... subscription_ids=["XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"],
... )
"""
logger.info("Setting Azure provider ...")
@@ -198,7 +262,7 @@ class AzureProvider(Provider):
)
# TODO: should we keep this here or within the identity?
self._locations = self.get_locations(self.session)
self._locations = self.get_locations()
# Audit Config
if config_content:
@@ -942,35 +1006,66 @@ class AzureProvider(Provider):
return identity
def get_locations(self, credentials) -> dict[str, list[str]]:
def get_locations(self) -> dict[str, list[str]]:
"""
Retrieves the locations available for each subscription using the provided credentials.
Args:
credentials: The credentials object used to authenticate the request.
Returns:
A dictionary containing the locations available for each subscription. The dictionary
has subscription display names as keys and lists of location names as values.
Examples:
>>> provider = AzureProvider(...)
>>> provider.get_locations()
{
'Subscription 1': ['eastus', 'eastus2', 'westus', 'westus2'],
'Subscription 2': ['eastus', 'eastus2', 'westus', 'westus2']
}
"""
locations = None
if credentials:
locations = {}
token = credentials.get_token("https://management.azure.com/.default").token
for display_name, subscription_id in self._identity.subscriptions.items():
locations.update({display_name: []})
url = f"https://management.azure.com/subscriptions/{subscription_id}/locations?api-version=2022-12-01"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
data = response.json()
for location in data["value"]:
locations[display_name].append(location["name"])
credentials = self.session
subscription_client = SubscriptionClient(credentials)
locations = {}
for display_name, subscription_id in self._identity.subscriptions.items():
locations[display_name] = []
# List locations for each subscription
for location in subscription_client.subscriptions.list_locations(
subscription_id
):
locations[display_name].append(location.name)
return locations
def get_regions(self, subscription_ids: Union[list[str], None] = None) -> set:
"""
Retrieves a set of regions available across all subscriptions or specific subscriptions if provided.
Args:
subscription_ids (List[str], optional): A list of subscription display names to filter the regions.
If None, regions from all subscriptions are returned.
Returns:
Set[str]: A set containing the unique regions available across the specified subscriptions.
Examples:
>>> provider = AzureProvider(...)
>>> provider.get_regions()
{'eastus', 'eastus2', 'westus', 'westus2'}
>>> provider.get_regions(subscription_ids=['Subscription 1'])
{'eastus', 'eastus2', 'westus', 'westus2'}
"""
locations = self.get_locations()
if subscription_ids is not None:
locations = {
sid: regions
for sid, regions in locations.items()
if sid in subscription_ids
}
return set(chain.from_iterable(locations.values()))
@staticmethod
def validate_static_credentials(
tenant_id: str = None, client_id: str = None, client_secret: str = None
@@ -9,7 +9,7 @@ class network_watcher_enabled(Check):
report = Check_Report_Azure(self.metadata())
report.subscription = subscription
report.resource_name = "Network Watcher"
report.location = "Global"
report.location = "global"
report.resource_id = f"/subscriptions/{network_client.subscriptions[subscription]}/resourceGroups/NetworkWatcherRG/providers/Microsoft.Network/networkWatchers/NetworkWatcher_*"
missing_locations = set(network_client.locations[subscription]) - set(
@@ -15,8 +15,8 @@ class GCPBaseException(ProwlerException):
"remediation": "Check the HTTP error and ensure the request is properly formatted.",
},
(3002, "GCPNoAccesibleProjectsError"): {
"message": "No Project IDs can be accessed via Google Credentials",
"remediation": "Ensure the project is accessible and properly set up.",
"message": "No Project IDs are active or can be accessed via Google Credentials",
"remediation": "Ensure the project is active and accessible.",
},
(3003, "GCPSetUpSessionError"): {
"message": "Error setting up session",
+195 -13
View File
@@ -36,6 +36,31 @@ from prowler.providers.gcp.models import GCPIdentityInfo, GCPOrganization, GCPPr
class GcpProvider(Provider):
"""
GCP Provider class to handle the GCP provider
Attributes:
- _type: str -> The provider type
- _session: Credentials -> The GCP credentials session
- _project_ids: list -> The list of project IDs
- _excluded_project_ids: list -> The list of excluded project IDs
- _identity: GCPIdentityInfo -> The GCP identity info
- _audit_config: dict -> The audit config
- _mutelist: GCPMutelist -> The GCP mutelist
- audit_metadata: Audit_Metadata -> The audit metadata
Methods:
- __init__ -> GCP Provider
- setup_session -> Setup the GCP session
- test_connection -> Test the connection to GCP
- print_credentials -> Print the GCP credentials
- get_projects -> Get the projects accessible by the provided credentials
- update_projects_with_organizations -> Update the projects with organizations
- is_project_matching -> Check if the input project matches the project to match
- validate_static_arguments -> Validate the static arguments
- validate_project_id -> Validate the provider ID
"""
_type: str = "gcp"
_session: Credentials
_project_ids: list
@@ -81,6 +106,56 @@ class GcpProvider(Provider):
client_id: str
client_secret: str
refresh_token: str
Raises:
GCPNoAccesibleProjectsError if no project IDs can be accessed via Google Credentials
GCPSetUpSessionError if an error occurs during the setup session
GCPLoadCredentialsFromDictError if an error occurs during the loading credentials from dict
GCPGetProjectError if an error occurs during the get project
Returns:
None
Usage:
- Authentication: Prowler will use by default your User Account credentials, you can configure it using:
- Using the gcloud command:
- gcloud init to use a new account
- gcloud config set account <account> to use a specific account
- gcloud auth application-default login to use the Application Default Credentials
- Prowler will use the Application Default Credentials if no credentials are provided
- Using static credentials:
- Using the client_id, client_secret and refresh_token:
>>> GcpProvider(
... client_id="client_id",
... client_secret="client_secret",
... refresh_token="refresh_token"
... )
- Using a credentials file:
>>> GcpProvider(
... credentials_file="credentials_file"
... )
- Impersonating a service account: If you want to impersonate a GCP service account, you can use the impersonate_service_account parameter. For this method user must be authenticated:
>>> GcpProvider(
... impersonate_service_account="service_account"
... )
- Projects: The GCP provider supports multi-project, which means that is capable of scan all the Google Cloud projects the user has access to.
- If you want to scan a specific project(s), you can use the project-ids argument.
>>> GcpProvider(
... project_ids=["project_id1", "project_id2"]
... )
- If you want to exclude a specific project(s), you can use the excluded-project-ids argument.
>>> GcpProvider(
... excluded_project_ids=["project_id1", "project_id2"]
... )
* Note: You can use asterisk * to exclude projects that match a pattern. For example, using "sys*" will exclude all the projects that start with sys.
- If you want to list all the available project IDs, you can use the list-project-ids argument.
>>> GcpProvider(
... list_project_ids=True
... )
- Organizations: If you want to scan a specific organization, you can use the organization-id argument. With this argument, Prowler will scan all the projects under that organization.
>>> GcpProvider(
... organization_id="organization_id"
... )
"""
logger.info("Instantiating GCP Provider ...")
self._impersonated_service_account = impersonate_service_account
@@ -105,22 +180,28 @@ class GcpProvider(Provider):
file=__file__,
message="No Project IDs can be accessed via Google Credentials.",
)
if project_ids:
if self._default_project_id not in project_ids:
self._default_project_id = project_ids[0]
for input_project in project_ids:
for accessible_project in accessible_projects:
if self.is_project_matching(input_project, accessible_project):
self._projects[accessible_project] = accessible_projects[
accessible_project
]
self._project_ids.append(
accessible_projects[accessible_project].id
)
for (
accessible_project_id,
accessible_project,
) in accessible_projects.items():
# Only scan active projects
if accessible_project.lifecycle_state == "ACTIVE":
if self.is_project_matching(
input_project, accessible_project_id
):
self._projects[accessible_project_id] = accessible_project
self._project_ids.append(accessible_project_id)
else:
# If not projects were input, all accessible projects are scanned by default
for project_id, project in accessible_projects.items():
self._projects[project_id] = project
self._project_ids.append(project_id)
# Only scan active projects
if project.lifecycle_state == "ACTIVE":
self._projects[project_id] = project
self._project_ids.append(project_id)
# Remove excluded projects if any input
if excluded_project_ids:
@@ -134,11 +215,11 @@ class GcpProvider(Provider):
if not self._projects:
logger.critical(
"No Input Project IDs can be accessed via Google Credentials."
"No Input Project IDs are active or can be accessed via Google Credentials."
)
raise GCPNoAccesibleProjectsError(
file=__file__,
message="No Input Project IDs can be accessed via Google Credentials.",
message="No Input Project IDs are active or can be accessed via Google Credentials.",
)
if list_project_ids:
@@ -233,11 +314,21 @@ class GcpProvider(Provider):
) -> tuple:
"""
Setup the GCP session with the provided credentials file or service account to impersonate
Args:
credentials_file: str
service_account: str
Returns:
Credentials object and default project ID
Raises:
GCPLoadCredentialsFromDictError if an error occurs during the loading credentials from dict
GCPSetUpSessionError if an error occurs during the setup session
Usage:
>>> GcpProvider.setup_session(credentials_file, service_account)
>>> GcpProvider.setup_session(service_account, gcp_credentials)
"""
try:
scopes = ["https://www.googleapis.com/auth/cloud-platform"]
@@ -305,6 +396,7 @@ class GcpProvider(Provider):
If the connection is successful, return a Connection object with is_connected set to True. If the connection fails, return a Connection object with error set to the exception.
Raise an exception if raise_on_exception is set to True.
If the Cloud Resource Manager API has not been used before or it is disabled, log a critical message and return a Connection object with error set to the exception.
Args:
credentials_file: str
service_account: str
@@ -313,8 +405,35 @@ class GcpProvider(Provider):
client_secret: str
refresh_token: str
provider_id: Optional[str] -> The provider ID, for GCP it is the project ID
Returns:
Connection object with is_connected set to True if the connection is successful, or error set to the exception if the connection fails
Raises:
GCPLoadCredentialsFromDictError if an error occurs during the loading credentials from dict
GCPSetUpSessionError if an error occurs during the setup session
GCPCloudResourceManagerAPINotUsedError if the Cloud Resource Manager API has not been used before or it is disabled
GCPInvalidProviderIdError if the provider ID does not match with the expected project_id
GCPTestConnectionError if an error occurs during the test connection
Usage:
- Using static credentials:
>>> GcpProvider.test_connection(
... client_id="client_id",
... client_secret="client_secret",
... refresh_token="refresh_token"
... )
- Using a credentials file:
>>> GcpProvider.test_connection(
... credentials_file="credentials_file"
... )
- Using a service account to impersonate, authentication is required to impersonate a service account:
>>> GcpProvider.test_connection(
... client_id="client_id",
... client_secret="client_secret",
... refresh_token="refresh_token",
... service_account="service_account"
... )
"""
try:
# Set the GCP credentials using the provided client_id, client_secret and refresh_token
@@ -415,11 +534,22 @@ class GcpProvider(Provider):
) -> dict[str, GCPProject]:
"""
Get the projects accessible by the provided credentials. If an organization ID is provided, only the projects under that organization are returned.
Args:
credentials: Credentials
organization_id: str
Returns:
dict[str, GCPProject]
Raises:
GCPCloudResourceManagerAPINotUsedError if the Cloud Resource Manager API has not been used before or it is disabled
GCPCloudAssetAPINotUsedError if the Cloud Asset API has not been used before or it is disabled
GCPHTTPError if an error occurs during the HTTP request
GCPGetProjectError if an error occurs during the get project
Usage:
>>> GcpProvider.get_projects(credentials=credentials, organization_id=organization_id)
"""
try:
projects = {}
@@ -535,6 +665,19 @@ class GcpProvider(Provider):
return projects
def update_projects_with_organizations(self):
"""
Update the projects with organizations
Returns:
None
Raises:
GCPHTTPError if an error occurs during the HTTP request
GCPGetProjectError if an error occurs during the get project
Usage:
>>> GcpProvider.update_projects_with_organizations()
"""
try:
service = discovery.build(
"cloudresourcemanager", "v1", credentials=self._session
@@ -571,11 +714,16 @@ class GcpProvider(Provider):
def is_project_matching(self, input_project: str, project_to_match: str) -> bool:
"""
Check if the input project matches the project to match
Args:
input_project: str
project_to_match: str
Returns:
bool
Usage:
>>> GcpProvider.is_project_matching(input_project, project_to_match)
"""
return (
"*" in input_project
@@ -602,6 +750,9 @@ class GcpProvider(Provider):
Raises:
GCPStaticCredentialsError if any of the static arguments is missing
Usage:
>>> GcpProvider.validate_static_arguments(client_id, client_secret, refresh_token)
"""
if not client_id or not client_secret or not refresh_token:
@@ -631,6 +782,9 @@ class GcpProvider(Provider):
Raises:
GCPInvalidProviderIdError if the provider ID does not match with the expected project_id
Usage:
>>> GcpProvider.validate_project_id(provider_id, credentials)
"""
available_projects = list(
@@ -647,3 +801,31 @@ class GcpProvider(Provider):
file=__file__,
message="The provider ID does not match with the expected project_id.",
)
def get_regions(self) -> set:
"""
Get the regions available in GCP for the given project IDs
Returns:
set of regions
"""
try:
regions = set()
service = discovery.build("compute", "v1", credentials=self._session)
for project_id in self._project_ids:
try:
request = service.regions().list(project=project_id)
response = request.execute()
for region in response.get("items", []):
regions.add(region["name"])
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
continue
return regions
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return set()
@@ -34,7 +34,7 @@ class IAM(GCPService):
ServiceAccount(
name=account["name"],
email=account["email"],
display_name=account["displayName"],
display_name=account.get("displayName", ""),
project_id=project_id,
)
)
@@ -33,6 +33,28 @@ from prowler.providers.kubernetes.models import (
class KubernetesProvider(Provider):
"""
Represents the Kubernetes provider.
Attributes:
_type (str): The provider type, wich is 'kubernetes'.
_session (KubernetesSession): The Kubernetes session.
_namespaces (list): The list of namespaces to audit.
_audit_config (dict): The audit configuration.
_identity (KubernetesIdentityInfo): The Kubernetes identity information.
_mutelist (dict): The mutelist.
audit_metadata (Audit_Metadata): The audit metadata.
Methods:
setup_session: Sets up the Kubernetes session.
test_connection: Tests the connection to the Kubernetes cluster.
search_and_save_roles: Searches for and saves roles.
get_context_user_roles: Retrieves the context user roles.
get_all_namespaces: Retrieves all namespaces.
get_pod_current_namespace: Retrieves the current namespace from the pod's mounted service account info.
print_credentials: Prints the Kubernetes credentials.
"""
_type: str = "kubernetes"
_session: KubernetesSession
_namespaces: list
@@ -56,6 +78,7 @@ class KubernetesProvider(Provider):
):
"""
Initializes the KubernetesProvider instance.
Args:
kubeconfig_file (str): Path to the kubeconfig file.
kubeconfig_content (dict): Content of the kubeconfig file.
@@ -66,6 +89,60 @@ class KubernetesProvider(Provider):
fixer_config (dict): Fixer configuration.
mutelist_path (str): Path to the mutelist file.
mutelist_content (dict): Mutelist content.
Raises:
KubernetesCloudResourceManagerAPINotUsedError: If the Kubernetes Cloud Resource Manager API is not used.
KubernetesError: If an error occurs.
Returns:
KubernetesProvider: The KubernetesProvider instance.
Usage:
- Authentication: The provider can be instantiated in two ways:
1. Using the kubeconfig file.
>>> provider = KubernetesProvider(
... kubeconfig_file="~/.kube/config",
... context="my-context",
... namespace=["default"],
... )
2. Using the kubeconfig content.
>>> provider = KubernetesProvider(
... kubeconfig_content={"kubecofig": "content"},
... context="my-context",
... namespace=["default"],
... )
- Namespace and context: The provider can be instantiated with or without specifying the namespace and context.
- Without specifying the namespace:
>>> provider = KubernetesProvider(
... kubeconfig_file="~/.kube/config",
... context="my-context",
... )
- With specifying the namespace:
>>> provider = KubernetesProvider(
... kubeconfig_file="~/.kube/config",
... context="my-context",
... namespace=["default"],
... )
- With specifying the context:
>>> provider = KubernetesProvider(
... kubeconfig_file="~/.kube/config",
... context="my-context",
... namespace=["default"],
... )
- Configuration: The provider can be instantiated with or without specifying the configuration.
- Without specifying the configuration:
>>> provider = KubernetesProvider(
... kubeconfig_file="~/.kube/config",
... context="my-context",
... namespace=["default"],
... )
- With specifying the configuration:
>>> provider = KubernetesProvider(
... kubeconfig_file="~/.kube/config",
... context="my-context",
... namespace=["default"],
... config_path="path/to/config.yaml",
... )
"""
logger.info("Instantiating Kubernetes Provider ...")
@@ -160,6 +237,11 @@ class KubernetesProvider(Provider):
Returns:
Tuple: A tuple containing the API client and the context.
Raises:
KubernetesInvalidKubeConfigFileError: If the kubeconfig file is invalid.
KubernetesInvalidProviderIdError: If the provider ID is invalid.
KubernetesSetUpSessionError: If an error occurs while setting up the session.
"""
logger.info(f"Using kubeconfig file: {kubeconfig_file}")
try:
@@ -250,8 +332,45 @@ class KubernetesProvider(Provider):
namespace (str): Namespace name.
provider_id (str): Provider ID to use, in this case, the Kubernetes context.
raise_on_exception (bool): Whether to raise an exception on error.
Returns:
Connection: A Connection object.
Raises:
KubernetesInvalidKubeConfigFileError: If the kubeconfig file is invalid.
KubernetesInvalidProviderIdError: If the provider ID is invalid.
KubernetesSetUpSessionError: If an error occurs while setting up the session.
KubernetesAPIError: If an error occurs while testing the connection.
Usage:
- Using the kubeconfig file:
>>> connection = KubernetesProvider.test_connection(
... kubeconfig_file="~/.kube/config",
... namespace="default",
... provider_id="my-context",
... raise_on_exception=True,
... )
- Using the kubeconfig content:
>>> connection = KubernetesProvider.test_connection(
... kubeconfig_content={"kubecofig": "content"},
... namespace="default",
... provider_id="my-context",
... raise_on_exception=True,
... )
- Using the namespace:
>>> connection = KubernetesProvider.test_connection(
... kubeconfig_file="~/.kube/config",
... namespace="default",
... provider_id="my-context",
... raise_on_exception=True,
... )
- Without raising an exception:
>>> connection = KubernetesProvider.test_connection(
... kubeconfig_file="~/.kube/config",
... namespace="default",
... provider_id="my-context",
... raise_on_exception=False,
... )
"""
try:
KubernetesProvider.setup_session(
@@ -316,6 +435,17 @@ class KubernetesProvider(Provider):
Returns:
list: A list containing the roles.
Raises:
KubernetesError: If an error occurs.
Usage:
>>> roles = self.search_and_save_roles(
... roles=[],
... role_bindings=rbac_api.list_cluster_role_binding().items,
... context_user="my-user",
... role_binding_type="ClusterRole",
... )
"""
try:
for rb in role_bindings:
@@ -343,6 +473,12 @@ class KubernetesProvider(Provider):
Returns:
list: A list containing the context user roles.
Raises:
KubernetesError: If an error occurs.
Usage:
>>> roles = self.get_context_user_roles()
"""
try:
rbac_api = client.RbacAuthorizationV1Api()
@@ -379,8 +515,17 @@ class KubernetesProvider(Provider):
def get_all_namespaces(self) -> list[str]:
"""
Retrieves all namespaces.
Returns:
list: A list containing all namespace names.
Raises:
KubernetesAPIError: If an error occurs while retrieving the namespaces.
KubernetesTimeoutError: If a timeout occurs while retrieving the namespaces.
KubernetesError: If an error occurs.
Usage:
>>> namespaces = self.get_all_namespaces()
"""
try:
v1 = client.CoreV1Api()
@@ -413,8 +558,12 @@ class KubernetesProvider(Provider):
def get_pod_current_namespace(self):
"""
Retrieves the current namespace from the pod's mounted service account info.
Returns:
str: The current namespace.
Usage:
>>> namespace = self.get_pod_current_namespace()
"""
try:
with open(
@@ -430,6 +579,9 @@ class KubernetesProvider(Provider):
def print_credentials(self):
"""
Prints the Kubernetes credentials.
Usage:
>>> self.print_credentials()
"""
if self._identity.context == "In-Cluster":
report_lines = [
@@ -10,18 +10,38 @@ class core_seccomp_profile_docker_default(Check):
report.namespace = pod.namespace
report.resource_name = pod.name
report.resource_id = pod.uid
if (
pod_seccomp_correct = (
pod.security_context
and pod.security_context.seccomp_profile
and pod.security_context.seccomp_profile.type == "RuntimeDefault"
):
)
containers_seccomp_correct = True
# Check container-level seccomp profile
for container in pod.containers.values():
if not (
container.security_context
and container.security_context.seccomp_profile
and container.security_context.seccomp_profile.type
== "RuntimeDefault"
):
containers_seccomp_correct = False
break
# Determine the report status
if pod_seccomp_correct or containers_seccomp_correct:
report.status = "PASS"
report.status_extended = (
f"Pod {pod.name} has docker/default seccomp profile enabled."
)
report.status_extended = f"Pod {pod.name} and its containers have docker/default seccomp profile enabled."
else:
report.status = "FAIL"
report.status_extended = f"Pod {pod.name} does not have docker/default seccomp profile enabled."
if not pod_seccomp_correct and not containers_seccomp_correct:
report.status_extended = f"Pod {pod.name} does not have docker/default seccomp profile enabled at both pod and container levels."
elif not pod_seccomp_correct:
report.status_extended = f"Pod {pod.name} does not have docker/default seccomp profile enabled at pod level."
else:
report.status_extended = f"Pod {pod.name} does not have docker/default seccomp profile enabled at container level."
findings.append(report)
return findings
+9 -10
View File
@@ -34,11 +34,11 @@ azure-mgmt-applicationinsights = "4.0.0"
azure-mgmt-authorization = "4.0.0"
azure-mgmt-compute = "33.0.0"
azure-mgmt-containerregistry = "10.3.0"
azure-mgmt-containerservice = "32.1.0"
azure-mgmt-containerservice = "33.0.0"
azure-mgmt-cosmosdb = "9.6.0"
azure-mgmt-keyvault = "10.3.1"
azure-mgmt-monitor = "6.0.2"
azure-mgmt-network = "27.0.0"
azure-mgmt-network = "28.0.0"
azure-mgmt-rdbms = "10.1.0"
azure-mgmt-resource = "23.2.0"
azure-mgmt-security = "7.0.0"
@@ -46,23 +46,22 @@ azure-mgmt-sql = "3.0.1"
azure-mgmt-storage = "21.2.1"
azure-mgmt-subscription = "3.1.1"
azure-mgmt-web = "7.3.1"
azure-storage-blob = "12.23.1"
boto3 = "1.35.57"
botocore = "1.35.58"
azure-storage-blob = "12.24.0"
boto3 = "1.35.63"
botocore = "1.35.64"
colorama = "0.4.6"
cryptography = "43.0.1"
dash = "2.18.2"
dash-bootstrap-components = "1.6.0"
detect-secrets = "1.5.0"
google-api-python-client = "2.151.0"
google-api-python-client = "2.153.0"
google-auth-httplib2 = ">=0.1,<0.3"
jsonschema = "4.23.0"
kubernetes = "31.0.0"
microsoft-kiota-abstractions = "1.6.0"
msgraph-sdk = "1.11.0"
microsoft-kiota-abstractions = "1.6.2"
msgraph-sdk = "1.12.0"
numpy = "2.0.2"
pandas = "2.2.3"
presidio-analyzer = "2.2.355"
py-ocsf-models = "0.2.0"
pydantic = "1.10.18"
pygithub = "2.5.0"
@@ -78,7 +77,7 @@ tzlocal = "5.2"
[tool.poetry.group.dev.dependencies]
bandit = "1.7.10"
black = "24.10.0"
coverage = "7.6.4"
coverage = "7.6.7"
docker = "7.1.0"
flake8 = "7.1.1"
freezegun = "1.5.1"
-24
View File
@@ -11,7 +11,6 @@ from prowler.config.config import (
load_and_validate_config_file,
load_and_validate_fixer_config_file,
)
from prowler.providers.aws.aws_provider import get_aws_available_regions
MOCK_PROWLER_VERSION = "3.3.0"
MOCK_OLD_PROWLER_VERSION = "0.0.0"
@@ -33,16 +32,6 @@ old_config_aws = {
"ec2_allowed_instance_owners": ["amazon-elb"],
"trusted_account_ids": [],
"log_group_retention_days": 365,
"critical_pii_entities": [
"CREDIT_CARD", # Credit card numbers are highly sensitive financial information.
"CRYPTO", # Crypto wallet numbers (e.g., Bitcoin addresses) can give access to cryptocurrency.
"IBAN_CODE", # International Bank Account Numbers are critical financial information.
"US_BANK_NUMBER", # US bank account numbers are sensitive and should be protected.
"US_SSN", # US Social Security Numbers are critical PII used for identity verification.
"US_PASSPORT", # US passport numbers can be used for identity theft.
"US_ITIN", # US Individual Taxpayer Identification Numbers are sensitive personal identifiers.
],
"pii_language": "en", # Language for recognizing PII entities
"max_idle_disconnect_timeout_in_seconds": 600,
"max_disconnect_timeout_in_seconds": 300,
"max_session_duration_seconds": 36000,
@@ -107,16 +96,6 @@ config_aws = {
"fargate_windows_latest_version": "1.0.0",
"trusted_account_ids": [],
"log_group_retention_days": 365,
"critical_pii_entities": [
"CREDIT_CARD", # Credit card numbers are highly sensitive financial information.
"CRYPTO", # Crypto wallet numbers (e.g., Bitcoin addresses) can give access to cryptocurrency.
"IBAN_CODE", # International Bank Account Numbers are critical financial information.
"US_BANK_NUMBER", # US bank account numbers are sensitive and should be protected.
"US_SSN", # US Social Security Numbers are critical PII used for identity verification.
"US_PASSPORT", # US passport numbers can be used for identity theft.
"US_ITIN", # US Individual Taxpayer Identification Numbers are sensitive personal identifiers.
],
"pii_language": "en", # Language for recognizing PII entities
"max_idle_disconnect_timeout_in_seconds": 600,
"max_disconnect_timeout_in_seconds": 300,
"max_session_duration_seconds": 36000,
@@ -366,9 +345,6 @@ config_kubernetes = {
class Test_Config:
def test_get_aws_available_regions(self):
assert len(get_aws_available_regions()) == 34
@mock.patch(
"prowler.config.config.requests.get", new=mock_prowler_get_latest_release
)
-25
View File
@@ -72,31 +72,6 @@ aws:
# AWS Cloudwatch Configuration
# aws.cloudwatch_log_group_retention_policy_specific_days_enabled --> by default is 365 days
log_group_retention_days: 365
# aws.cloudwatch_log_group_no_critical_pii_in_logs --> see all available entities in https://microsoft.github.io/presidio/supported_entities/
critical_pii_entities : [
"CREDIT_CARD", # Credit card numbers are highly sensitive financial information.
"CRYPTO", # Crypto wallet numbers (e.g., Bitcoin addresses) can give access to cryptocurrency.
"IBAN_CODE", # International Bank Account Numbers are critical financial information.
"US_BANK_NUMBER", # US bank account numbers are sensitive and should be protected.
"US_SSN", # US Social Security Numbers are critical PII used for identity verification.
"US_PASSPORT", # US passport numbers can be used for identity theft.
"US_ITIN", # US Individual Taxpayer Identification Numbers are sensitive personal identifiers.
#"UK_NHS", # UK NHS numbers can be used to access medical records and other private information.
#"ES_NIF", # Spanish NIF (Personal tax ID) is critical for identification and tax purposes.
#"ES_NIE", # Spanish NIE (Foreigners ID card) is a critical identifier for foreign residents.
#"IT_FISCAL_CODE", # Italian personal identification code is sensitive PII for tax and legal purposes.
#"IT_PASSPORT", # Italian passport numbers are critical PII.
#"IT_IDENTITY_CARD", # Italian identity card numbers are critical for personal identification.
#"PL_PESEL", # Polish PESEL numbers are sensitive personal identifiers.
#"SG_NRIC_FIN", # Singapore National Registration Identification Card is critical PII.
#"AU_ABN", # Australian Business Numbers are critical for business identification.
#"AU_TFN", # Australian Tax File Numbers are sensitive and used for taxation purposes.
#"AU_MEDICARE", # Australian Medicare numbers are sensitive medical identifiers.
#"IN_PAN", # Indian Permanent Account Numbers are critical for tax purposes and identity.
#"IN_AADHAAR", # Indian Aadhaar numbers are highly sensitive and serve as a universal identity number.
#"FI_PERSONAL_IDENTITY_CODE" # Finnish Personal Identity Code is sensitive PII for personal identification.
]
pii_language: "en" # Language for recognizing PII entities
# AWS AppStream Session Configuration
# aws.appstream_fleet_session_idle_disconnect_timeout
-25
View File
@@ -28,31 +28,6 @@ trusted_account_ids: []
# AWS Cloudwatch Configuration
# aws.cloudwatch_log_group_retention_policy_specific_days_enabled --> by default is 365 days
log_group_retention_days: 365
# aws.cloudwatch_log_group_no_critical_pii_in_logs --> see all available entities in https://microsoft.github.io/presidio/supported_entities/
critical_pii_entities : [
"CREDIT_CARD", # Credit card numbers are highly sensitive financial information.
"CRYPTO", # Crypto wallet numbers (e.g., Bitcoin addresses) can give access to cryptocurrency.
"IBAN_CODE", # International Bank Account Numbers are critical financial information.
"US_BANK_NUMBER", # US bank account numbers are sensitive and should be protected.
"US_SSN", # US Social Security Numbers are critical PII used for identity verification.
"US_PASSPORT", # US passport numbers can be used for identity theft.
"US_ITIN", # US Individual Taxpayer Identification Numbers are sensitive personal identifiers.
#"UK_NHS", # UK NHS numbers can be used to access medical records and other private information.
#"ES_NIF", # Spanish NIF (Personal tax ID) is critical for identification and tax purposes.
#"ES_NIE", # Spanish NIE (Foreigners ID card) is a critical identifier for foreign residents.
#"IT_FISCAL_CODE", # Italian personal identification code is sensitive PII for tax and legal purposes.
#"IT_PASSPORT", # Italian passport numbers are critical PII.
#"IT_IDENTITY_CARD", # Italian identity card numbers are critical for personal identification.
#"PL_PESEL", # Polish PESEL numbers are sensitive personal identifiers.
#"SG_NRIC_FIN", # Singapore National Registration Identification Card is critical PII.
#"AU_ABN", # Australian Business Numbers are critical for business identification.
#"AU_TFN", # Australian Tax File Numbers are sensitive and used for taxation purposes.
#"AU_MEDICARE", # Australian Medicare numbers are sensitive medical identifiers.
#"IN_PAN", # Indian Permanent Account Numbers are critical for tax purposes and identity.
#"IN_AADHAAR", # Indian Aadhaar numbers are highly sensitive and serve as a universal identity number.
#"FI_PERSONAL_IDENTITY_CODE" # Finnish Personal Identity Code is sensitive PII for personal identification.
]
pii_language: "en" # Language for recognizing PII entities
# AWS AppStream Session Configuration
# aws.appstream_fleet_session_idle_disconnect_timeout
@@ -30,7 +30,7 @@ class TestAzureCIS:
output_data = output.data[0]
assert isinstance(output_data, AzureCISModel)
assert output_data.Provider == "azure"
assert output_data.Subscription == AZURE_SUBSCRIPTION_NAME
assert output_data.SubscriptionId == AZURE_SUBSCRIPTION_ID
assert output_data.Location == ""
assert output_data.Description == CIS_2_0_AZURE.Description
assert output_data.Requirements_Id == CIS_2_0_AZURE.Requirements[0].Id
@@ -91,7 +91,7 @@ class TestAzureCIS:
# Test manual check
output_data_manual = output.data[1]
assert output_data_manual.Provider == "azure"
assert output_data_manual.Subscription == ""
assert output_data_manual.SubscriptionId == ""
assert output_data_manual.Location == ""
assert output_data_manual.Description == CIS_2_0_AZURE.Description
assert output_data_manual.Requirements_Id == CIS_2_0_AZURE.Requirements[1].Id
@@ -171,5 +171,5 @@ class TestAzureCIS:
mock_file.seek(0)
content = mock_file.read()
expected_csv = f"PROVIDER;DESCRIPTION;SUBSCRIPTION;LOCATION;ASSESSMENTDATE;REQUIREMENTS_ID;REQUIREMENTS_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_SECTION;REQUIREMENTS_ATTRIBUTES_PROFILE;REQUIREMENTS_ATTRIBUTES_ASSESSMENTSTATUS;REQUIREMENTS_ATTRIBUTES_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_RATIONALESTATEMENT;REQUIREMENTS_ATTRIBUTES_IMPACTSTATEMENT;REQUIREMENTS_ATTRIBUTES_REMEDIATIONPROCEDURE;REQUIREMENTS_ATTRIBUTES_AUDITPROCEDURE;REQUIREMENTS_ATTRIBUTES_ADDITIONALINFORMATION;REQUIREMENTS_ATTRIBUTES_DEFAULTVALUE;REQUIREMENTS_ATTRIBUTES_REFERENCES;STATUS;STATUSEXTENDED;RESOURCEID;RESOURCENAME;CHECKID;MUTED\r\nazure;The CIS Azure Foundations Benchmark provides prescriptive guidance for configuring security options for a subset of Azure with an emphasis on foundational, testable, and architecture agnostic settings.;Subscription Name;;{datetime.now()};2.1.3;Ensure That Microsoft Defender for Databases Is Set To 'On';2.1 Microsoft Defender for Cloud;Level 2;Manual;Turning on Microsoft Defender for Databases enables threat detection for the instances running your database software. This provides threat intelligence, anomaly detection, and behavior analytics in the Azure Microsoft Defender for Cloud. Instead of being enabled on services like Platform as a Service (PaaS), this implementation will run within your instances as Infrastructure as a Service (IaaS) on the Operating Systems hosting your databases.;Enabling Microsoft Defender for Azure SQL Databases allows your organization more granular control of the infrastructure running your database software. Instead of waiting on Microsoft release updates or other similar processes, you can manage them yourself. Threat detection is provided by the Microsoft Security Response Center (MSRC).;Running Defender on Infrastructure as a service (IaaS) may incur increased costs associated with running the service and the instance it is on. Similarly, you will need qualified personnel to maintain the operating system and software updates. If it is not maintained, security patches will not be applied and it may be open to vulnerabilities.;From Azure Portal 1. Go to Microsoft Defender for Cloud 2. Select Environment Settings 3. Click on the subscription name 4. Select Defender plans 5. Set Databases Status to On 6. Select Save Review the chosen pricing tier. For the Azure Databases resource review the different plan information and choose one that fits the needs of your organization. From Azure CLI Run the following commands: az security pricing create -n 'SqlServers' --tier 'Standard' az security pricing create -n 'SqlServerVirtualMachines' --tier 'Standard' az security pricing create -n 'OpenSourceRelationalDatabases' --tier 'Standard' az security pricing create -n 'CosmosDbs' --tier 'Standard' From Azure PowerShell Run the following commands: Set-AzSecurityPricing -Name 'SqlServers' -PricingTier 'Standard' Set-AzSecurityPricing -Name 'SqlServerVirtualMachines' -PricingTier 'Standard' Set-AzSecurityPricing -Name 'OpenSourceRelationalDatabases' -PricingTier 'Standard' Set-AzSecurityPricing -Name 'CosmosDbs' -PricingTier 'Standard';From Azure Portal 1. Go to Microsoft Defender for Cloud 2. Select Environment Settings 3. Click on the subscription name 4. Select Defender plans 5. Ensure Databases Status is set to On 6. Review the chosen pricing tier From Azure CLI Ensure the output of the below commands is Standard az security pricing show -n 'SqlServers' az security pricing show -n 'SqlServerVirtualMachines' az security pricing show -n 'OpenSourceRelationalDatabases' az security pricing show -n 'CosmosDbs' If the output of any of the above commands shows pricingTier with a value of Free, the setting is out of compliance. From PowerShell Connect-AzAccount Get-AzSecurityPricing |select-object Name,PricingTier |where-object {{$_.Name -match 'Sql' -or $_.Name -match 'Cosmos' -or $_.Name -match 'OpenSource'}} Ensure the output shows Standard for each database type under the PricingTier column. Any that show Free are considered out of compliance.;;By default, Microsoft Defender plan is off.;https://docs.microsoft.com/en-us/azure/azure-sql/database/azure-defender-for-sql?view=azuresql:https://docs.microsoft.com/en-us/azure/defender-for-cloud/quickstart-enable-database-protections:https://docs.microsoft.com/en-us/azure/defender-for-cloud/defender-for-databases-usage:https://docs.microsoft.com/en-us/azure/security-center/security-center-detection-capabilities:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/list:https://docs.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-logging-threat-detection#lt-1-enable-threat-detection-capabilities;PASS;;;;test-check-id;False\r\nazure;The CIS Azure Foundations Benchmark provides prescriptive guidance for configuring security options for a subset of Azure with an emphasis on foundational, testable, and architecture agnostic settings.;;;{datetime.now()};2.1.4;Ensure That Microsoft Defender for Databases Is Set To 'On'Line truncated
expected_csv = f"PROVIDER;DESCRIPTION;SUBSCRIPTIONID;LOCATION;ASSESSMENTDATE;REQUIREMENTS_ID;REQUIREMENTS_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_SECTION;REQUIREMENTS_ATTRIBUTES_PROFILE;REQUIREMENTS_ATTRIBUTES_ASSESSMENTSTATUS;REQUIREMENTS_ATTRIBUTES_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_RATIONALESTATEMENT;REQUIREMENTS_ATTRIBUTES_IMPACTSTATEMENT;REQUIREMENTS_ATTRIBUTES_REMEDIATIONPROCEDURE;REQUIREMENTS_ATTRIBUTES_AUDITPROCEDURE;REQUIREMENTS_ATTRIBUTES_ADDITIONALINFORMATION;REQUIREMENTS_ATTRIBUTES_DEFAULTVALUE;REQUIREMENTS_ATTRIBUTES_REFERENCES;STATUS;STATUSEXTENDED;RESOURCEID;RESOURCENAME;CHECKID;MUTED\r\nazure;The CIS Azure Foundations Benchmark provides prescriptive guidance for configuring security options for a subset of Azure with an emphasis on foundational, testable, and architecture agnostic settings.;{AZURE_SUBSCRIPTION_ID};;{datetime.now()};2.1.3;Ensure That Microsoft Defender for Databases Is Set To 'On';2.1 Microsoft Defender for Cloud;Level 2;Manual;Turning on Microsoft Defender for Databases enables threat detection for the instances running your database software. This provides threat intelligence, anomaly detection, and behavior analytics in the Azure Microsoft Defender for Cloud. Instead of being enabled on services like Platform as a Service (PaaS), this implementation will run within your instances as Infrastructure as a Service (IaaS) on the Operating Systems hosting your databases.;Enabling Microsoft Defender for Azure SQL Databases allows your organization more granular control of the infrastructure running your database software. Instead of waiting on Microsoft release updates or other similar processes, you can manage them yourself. Threat detection is provided by the Microsoft Security Response Center (MSRC).;Running Defender on Infrastructure as a service (IaaS) may incur increased costs associated with running the service and the instance it is on. Similarly, you will need qualified personnel to maintain the operating system and software updates. If it is not maintained, security patches will not be applied and it may be open to vulnerabilities.;From Azure Portal 1. Go to Microsoft Defender for Cloud 2. Select Environment Settings 3. Click on the subscription name 4. Select Defender plans 5. Set Databases Status to On 6. Select Save Review the chosen pricing tier. For the Azure Databases resource review the different plan information and choose one that fits the needs of your organization. From Azure CLI Run the following commands: az security pricing create -n 'SqlServers' --tier 'Standard' az security pricing create -n 'SqlServerVirtualMachines' --tier 'Standard' az security pricing create -n 'OpenSourceRelationalDatabases' --tier 'Standard' az security pricing create -n 'CosmosDbs' --tier 'Standard' From Azure PowerShell Run the following commands: Set-AzSecurityPricing -Name 'SqlServers' -PricingTier 'Standard' Set-AzSecurityPricing -Name 'SqlServerVirtualMachines' -PricingTier 'Standard' Set-AzSecurityPricing -Name 'OpenSourceRelationalDatabases' -PricingTier 'Standard' Set-AzSecurityPricing -Name 'CosmosDbs' -PricingTier 'Standard';From Azure Portal 1. Go to Microsoft Defender for Cloud 2. Select Environment Settings 3. Click on the subscription name 4. Select Defender plans 5. Ensure Databases Status is set to On 6. Review the chosen pricing tier From Azure CLI Ensure the output of the below commands is Standard az security pricing show -n 'SqlServers' az security pricing show -n 'SqlServerVirtualMachines' az security pricing show -n 'OpenSourceRelationalDatabases' az security pricing show -n 'CosmosDbs' If the output of any of the above commands shows pricingTier with a value of Free, the setting is out of compliance. From PowerShell Connect-AzAccount Get-AzSecurityPricing |select-object Name,PricingTier |where-object {{$_.Name -match 'Sql' -or $_.Name -match 'Cosmos' -or $_.Name -match 'OpenSource'}} Ensure the output shows Standard for each database type under the PricingTier column. Any that show Free are considered out of compliance.;;By default, Microsoft Defender plan is off.;https://docs.microsoft.com/en-us/azure/azure-sql/database/azure-defender-for-sql?view=azuresql:https://docs.microsoft.com/en-us/azure/defender-for-cloud/quickstart-enable-database-protections:https://docs.microsoft.com/en-us/azure/defender-for-cloud/defender-for-databases-usage:https://docs.microsoft.com/en-us/azure/security-center/security-center-detection-capabilities:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/list:https://docs.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-logging-threat-detection#lt-1-enable-threat-detection-capabilities;PASS;;;;test-check-id;False\r\nazure;The CIS Azure Foundations Benchmark provides prescriptive guidance for configuring security options for a subset of Azure with an emphasis on foundational, testable, and architecture agnostic settings.;;;{datetime.now()};2.1.4;Ensure That Microsoft Defender for Databases Is SetLine truncated
assert content == expected_csv
+117 -7
View File
@@ -9,17 +9,14 @@ from unittest import mock
import botocore
import botocore.exceptions
import pytest
from boto3 import client, resource, session
from mock import patch
from moto import mock_aws
from pytest import raises
from tzlocal import get_localzone
from prowler.providers.aws.aws_provider import (
AwsProvider,
get_aws_available_regions,
get_aws_region_for_sts,
)
from prowler.providers.aws.aws_provider import AwsProvider, get_aws_region_for_sts
from prowler.providers.aws.config import (
AWS_STS_GLOBAL_ENDPOINT_REGION,
BOTO3_USER_AGENT_EXTRA,
@@ -28,6 +25,7 @@ from prowler.providers.aws.config import (
from prowler.providers.aws.exceptions.exceptions import (
AWSArgumentTypeValidationError,
AWSIAMRoleARNInvalidResourceTypeError,
AWSInvalidPartitionError,
AWSInvalidProviderIdError,
AWSNoCredentialsError,
)
@@ -1720,7 +1718,16 @@ aws:
)
assert not recovered_regions
def test_get_aws_available_regions(self):
def test_get_regions_all_count(self):
assert len(AwsProvider.get_regions(partition=None)) == 34
def test_get_regions_cn_count(self):
assert len(AwsProvider.get_regions("aws-cn")) == 2
def test_get_regions_aws_count(self):
assert len(AwsProvider.get_regions(partition="aws")) == 30
def test_get_all_regions(self):
with patch(
"prowler.providers.aws.aws_provider.read_aws_regions_file",
return_value={
@@ -1741,12 +1748,115 @@ aws:
}
},
):
assert get_aws_available_regions() == {
assert AwsProvider.get_regions(partition=None) == {
"af-south-1",
"cn-north-1",
"us-gov-west-1",
}
def test_get_regions_with_us_gov_partition(self):
with patch(
"prowler.providers.aws.aws_provider.read_aws_regions_file",
return_value={
"services": {
"acm": {
"regions": {
"aws": [
"af-south-1",
],
"aws-cn": [
"cn-north-1",
],
"aws-us-gov": [
"us-gov-west-1",
],
}
}
}
},
):
assert AwsProvider.get_regions("aws-us-gov") == {
"us-gov-west-1",
}
def test_get_regions_with_aws_partition(self):
with patch(
"prowler.providers.aws.aws_provider.read_aws_regions_file",
return_value={
"services": {
"acm": {
"regions": {
"aws": [
"af-south-1",
],
"aws-cn": [
"cn-north-1",
],
"aws-us-gov": [
"us-gov-west-1",
],
}
}
}
},
):
assert AwsProvider.get_regions("aws") == {
"af-south-1",
}
def test_get_regions_with_cn_partition(self):
with patch(
"prowler.providers.aws.aws_provider.read_aws_regions_file",
return_value={
"services": {
"acm": {
"regions": {
"aws": [
"af-south-1",
],
"aws-cn": [
"cn-north-1",
],
"aws-us-gov": [
"us-gov-west-1",
],
}
}
}
},
):
assert AwsProvider.get_regions("aws-cn") == {
"cn-north-1",
}
def test_get_regions_with_unknown_partition(self):
with patch(
"prowler.providers.aws.aws_provider.read_aws_regions_file",
return_value={
"services": {
"acm": {
"regions": {
"aws": [
"af-south-1",
],
"aws-cn": [
"cn-north-1",
],
"aws-us-gov": [
"us-gov-west-1",
],
}
}
}
},
):
partition = "unknown"
with pytest.raises(AWSInvalidPartitionError) as exception:
AwsProvider.get_regions(partition)
assert exception.type == AWSInvalidPartitionError
assert f"Invalid partition: {partition}" in exception.value.args[0]
def test_get_aws_region_for_sts_input_regions_none_session_region_none(self):
input_regions = None
session_region = None
@@ -1,206 +0,0 @@
from re import search
from unittest import mock
from boto3 import client
from moto import mock_aws
from moto.core.utils import unix_time_millis
from tests.providers.aws.utils import (
AWS_REGION_EU_WEST_1,
AWS_REGION_US_EAST_1,
set_mocked_aws_provider,
)
class Test_cloudwatch_log_group_no_critical_pii_in_logs:
def test_cloudwatch_no_log_groups(self):
from prowler.providers.aws.services.cloudwatch.cloudwatch_service import Logs
aws_provider = set_mocked_aws_provider(
[AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1]
)
from prowler.providers.common.models import Audit_Metadata
aws_provider.audit_metadata = Audit_Metadata(
services_scanned=0,
# We need to set this check to call _describe_log_groups
expected_checks=["cloudwatch_log_group_no_critical_pii_in_logs"],
completed_checks=0,
audit_progress=0,
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), mock.patch(
"prowler.providers.aws.services.cloudwatch.cloudwatch_log_group_no_critical_pii_in_logs.cloudwatch_log_group_no_critical_pii_in_logs.logs_client",
new=Logs(aws_provider),
):
# Test Check
from prowler.providers.aws.services.cloudwatch.cloudwatch_log_group_no_critical_pii_in_logs.cloudwatch_log_group_no_critical_pii_in_logs import (
cloudwatch_log_group_no_critical_pii_in_logs,
)
check = cloudwatch_log_group_no_critical_pii_in_logs()
result = check.execute()
assert len(result) == 0
@mock_aws
def test_cloudwatch_log_group_without_pii(self):
# Generate Logs Client
logs_client = client("logs", region_name=AWS_REGION_US_EAST_1)
# Request Logs group
logs_client.create_log_group(logGroupName="test", tags={"test": "test"})
logs_client.create_log_stream(logGroupName="test", logStreamName="test stream")
logs_client.put_log_events(
logGroupName="test",
logStreamName="test stream",
logEvents=[
{
"timestamp": int(unix_time_millis()),
"message": "non sensitive message",
}
],
)
from prowler.providers.aws.services.cloudwatch.cloudwatch_service import Logs
aws_provider = set_mocked_aws_provider(
[AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1]
)
from prowler.providers.common.models import Audit_Metadata
aws_provider.audit_metadata = Audit_Metadata(
services_scanned=0,
# We need to set this check to call _describe_log_groups
expected_checks=["cloudwatch_log_group_no_critical_pii_in_logs"],
completed_checks=0,
audit_progress=0,
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), mock.patch(
"prowler.providers.aws.services.cloudwatch.cloudwatch_log_group_no_critical_pii_in_logs.cloudwatch_log_group_no_critical_pii_in_logs.logs_client",
new=Logs(aws_provider),
):
# Test Check
from prowler.providers.aws.services.cloudwatch.cloudwatch_log_group_no_critical_pii_in_logs.cloudwatch_log_group_no_critical_pii_in_logs import (
cloudwatch_log_group_no_critical_pii_in_logs,
)
check = cloudwatch_log_group_no_critical_pii_in_logs()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended == "No critical PII found in test log group."
)
assert result[0].resource_id == "test"
assert (
result[0].resource_arn
== f"arn:aws:logs:{AWS_REGION_US_EAST_1}:123456789012:log-group:test"
)
assert result[0].region == AWS_REGION_US_EAST_1
assert result[0].resource_tags == [{"test": "test"}]
@mock_aws
def test_cloudwatch_log_group_with_pii(self):
# Generate Logs Client
logs_client = client("logs", region_name=AWS_REGION_US_EAST_1)
# Request Logs group
logs_client.create_log_group(logGroupName="test", tags={"test": "test"})
logs_client.create_log_stream(logGroupName="test", logStreamName="test stream")
logs_client.put_log_events(
logGroupName="test",
logStreamName="test stream",
logEvents=[
{
"timestamp": int(unix_time_millis()),
"message": "Credit Card Number = 374752619478856",
}
],
)
from prowler.providers.aws.services.cloudwatch.cloudwatch_service import Logs
aws_provider = set_mocked_aws_provider(
[AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1]
)
from prowler.providers.common.models import Audit_Metadata
aws_provider.audit_metadata = Audit_Metadata(
services_scanned=0,
# We need to set this check to call _describe_log_groups
expected_checks=["cloudwatch_log_group_no_critical_pii_in_logs"],
completed_checks=0,
audit_progress=0,
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), mock.patch(
"prowler.providers.aws.services.cloudwatch.cloudwatch_log_group_no_critical_pii_in_logs.cloudwatch_log_group_no_critical_pii_in_logs.logs_client",
new=Logs(aws_provider),
):
# Test Check
from prowler.providers.aws.services.cloudwatch.cloudwatch_log_group_no_critical_pii_in_logs.cloudwatch_log_group_no_critical_pii_in_logs import (
cloudwatch_log_group_no_critical_pii_in_logs,
)
check = cloudwatch_log_group_no_critical_pii_in_logs()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert search(
"Potential critical PII found in log group", result[0].status_extended
)
assert result[0].resource_id == "test"
assert (
result[0].resource_arn
== f"arn:aws:logs:{AWS_REGION_US_EAST_1}:123456789012:log-group:test"
)
assert result[0].region == AWS_REGION_US_EAST_1
assert result[0].resource_tags == [{"test": "test"}]
@mock_aws
def test_access_denied(self):
from prowler.providers.aws.services.cloudwatch.cloudwatch_service import Logs
aws_provider = set_mocked_aws_provider(
[AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1]
)
from prowler.providers.common.models import Audit_Metadata
aws_provider.audit_metadata = Audit_Metadata(
services_scanned=0,
# We need to set this check to call _describe_log_groups
expected_checks=["cloudwatch_log_group_no_critical_pii_in_logs"],
completed_checks=0,
audit_progress=0,
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), mock.patch(
"prowler.providers.aws.services.cloudwatch.cloudwatch_log_group_no_critical_pii_in_logs.cloudwatch_log_group_no_critical_pii_in_logs.logs_client",
new=Logs(aws_provider),
) as logs_client:
# Test Check
from prowler.providers.aws.services.cloudwatch.cloudwatch_log_group_no_critical_pii_in_logs.cloudwatch_log_group_no_critical_pii_in_logs import (
cloudwatch_log_group_no_critical_pii_in_logs,
)
logs_client.log_groups = None
check = cloudwatch_log_group_no_critical_pii_in_logs()
result = check.execute()
assert len(result) == 0
@@ -72,7 +72,7 @@ class Test_ec2_ami_public:
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].status_extended == f"EC2 AMI {image_id} is not public."
assert result[0].status_extended == "EC2 AMI test-ami is not public."
assert result[0].resource_id == image_id
assert (
result[0].resource_arn
@@ -124,9 +124,7 @@ class Test_ec2_ami_public:
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended == f"EC2 AMI {image_id} is currently public."
)
assert result[0].status_extended == "EC2 AMI test-ami is currently public."
assert result[0].resource_id == image_id
assert (
result[0].resource_arn
@@ -0,0 +1,154 @@
from boto3 import client
from moto import mock_aws
from prowler.providers.aws.services.firehose.firehose_service import (
EncryptionStatus,
Firehose,
)
from tests.providers.aws.utils import (
AWS_ACCOUNT_NUMBER,
AWS_REGION_EU_WEST_1,
set_mocked_aws_provider,
)
class Test_Firehose_Service:
# Test Firehose Service
@mock_aws
def test_service(self):
# Firehose client for this test class
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
firehose = Firehose(aws_provider)
assert firehose.service == "firehose"
# Test Firehose Client
@mock_aws
def test_client(self):
# Firehose client for this test class
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
firehose = Firehose(aws_provider)
for regional_client in firehose.regional_clients.values():
assert regional_client.__class__.__name__ == "Firehose"
# Test Firehose Session
@mock_aws
def test__get_session__(self):
# Firehose client for this test class
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
firehose = Firehose(aws_provider)
assert firehose.session.__class__.__name__ == "Session"
# Test Firehose List Delivery Streams
@mock_aws
def test_list_delivery_streams(self):
# Generate S3 client
s3_client = client("s3", region_name=AWS_REGION_EU_WEST_1)
s3_client.create_bucket(
Bucket="test-bucket",
CreateBucketConfiguration={"LocationConstraint": AWS_REGION_EU_WEST_1},
)
# Generate Firehose client
firehose_client = client("firehose", region_name=AWS_REGION_EU_WEST_1)
delivery_stream = firehose_client.create_delivery_stream(
DeliveryStreamName="test-delivery-stream",
DeliveryStreamType="DirectPut",
S3DestinationConfiguration={
"RoleARN": "arn:aws:iam::012345678901:role/firehose-role",
"BucketARN": "arn:aws:s3:::test-bucket",
"Prefix": "",
"BufferingHints": {"IntervalInSeconds": 300, "SizeInMBs": 5},
"CompressionFormat": "UNCOMPRESSED",
},
Tags=[{"Key": "key", "Value": "value"}],
)
arn = delivery_stream["DeliveryStreamARN"]
delivery_stream_name = arn.split("/")[-1]
# Firehose Client for this test class
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
firehose = Firehose(aws_provider)
assert len(firehose.delivery_streams) == 1
assert firehose.delivery_streams[arn].arn == arn
assert firehose.delivery_streams[arn].name == delivery_stream_name
assert firehose.delivery_streams[arn].region == AWS_REGION_EU_WEST_1
assert firehose.delivery_streams[arn].tags == [{"Key": "key", "Value": "value"}]
@mock_aws
def test_list_tags_for_delivery_stream(self):
# Generate Firehose client
firehose_client = client("firehose", region_name=AWS_REGION_EU_WEST_1)
delivery_stream = firehose_client.create_delivery_stream(
DeliveryStreamName="test-delivery-stream",
DeliveryStreamType="DirectPut",
S3DestinationConfiguration={
"RoleARN": "arn:aws:iam::012345678901:role/firehose-role",
"BucketARN": "arn:aws:s3:::test-bucket",
"Prefix": "",
"BufferingHints": {"IntervalInSeconds": 300, "SizeInMBs": 5},
"CompressionFormat": "UNCOMPRESSED",
},
Tags=[{"Key": "key", "Value": "value"}],
)
arn = delivery_stream["DeliveryStreamARN"]
delivery_stream_name = arn.split("/")[-1]
# Firehose Client for this test class
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
firehose = Firehose(aws_provider)
assert len(firehose.delivery_streams) == 1
assert firehose.delivery_streams[arn].arn == arn
assert firehose.delivery_streams[arn].name == delivery_stream_name
assert firehose.delivery_streams[arn].region == AWS_REGION_EU_WEST_1
assert firehose.delivery_streams[arn].tags == [{"Key": "key", "Value": "value"}]
@mock_aws
def test_describe_delivery_stream(self):
# Generate S3 client
s3_client = client("s3", region_name=AWS_REGION_EU_WEST_1)
s3_client.create_bucket(
Bucket="test-bucket",
CreateBucketConfiguration={"LocationConstraint": AWS_REGION_EU_WEST_1},
)
# Generate Firehose client
firehose_client = client("firehose", region_name=AWS_REGION_EU_WEST_1)
delivery_stream = firehose_client.create_delivery_stream(
DeliveryStreamName="test-delivery-stream",
DeliveryStreamType="DirectPut",
S3DestinationConfiguration={
"RoleARN": "arn:aws:iam::012345678901:role/firehose-role",
"BucketARN": "arn:aws:s3:::test-bucket",
"Prefix": "",
"BufferingHints": {"IntervalInSeconds": 300, "SizeInMBs": 5},
"CompressionFormat": "UNCOMPRESSED",
},
Tags=[{"Key": "key", "Value": "value"}],
)
arn = delivery_stream["DeliveryStreamARN"]
delivery_stream_name = arn.split("/")[-1]
firehose_client.start_delivery_stream_encryption(
DeliveryStreamName=delivery_stream_name,
DeliveryStreamEncryptionConfigurationInput={
"KeyARN": f"arn:aws:kms:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:key/test-kms-key-id",
"KeyType": "CUSTOMER_MANAGED_CMK",
},
)
# Firehose Client for this test class
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
firehose = Firehose(aws_provider)
assert len(firehose.delivery_streams) == 1
assert firehose.delivery_streams[arn].arn == arn
assert firehose.delivery_streams[arn].name == delivery_stream_name
assert firehose.delivery_streams[arn].region == AWS_REGION_EU_WEST_1
assert firehose.delivery_streams[arn].tags == [{"Key": "key", "Value": "value"}]
assert firehose.delivery_streams[arn].kms_encryption == EncryptionStatus.ENABLED
assert (
firehose.delivery_streams[arn].kms_key_arn
== f"arn:aws:kms:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:key/test-kms-key-id"
)
@@ -0,0 +1,195 @@
from unittest import mock
from boto3 import client
from moto import mock_aws
from tests.providers.aws.utils import (
AWS_ACCOUNT_NUMBER,
AWS_REGION_EU_WEST_1,
set_mocked_aws_provider,
)
class Test_firehose_stream_encrypted_at_rest:
@mock_aws
def test_no_streams(self):
from prowler.providers.aws.services.firehose.firehose_service import Firehose
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), mock.patch(
"prowler.providers.aws.services.firehose.firehose_stream_encrypted_at_rest.firehose_stream_encrypted_at_rest.firehose_client",
new=Firehose(aws_provider),
):
# Test Check
from prowler.providers.aws.services.firehose.firehose_stream_encrypted_at_rest.firehose_stream_encrypted_at_rest import (
firehose_stream_encrypted_at_rest,
)
check = firehose_stream_encrypted_at_rest()
result = check.execute()
assert len(result) == 0
@mock_aws
def test_stream_kms_encryption_enabled(self):
# Generate S3 client
s3_client = client("s3", region_name=AWS_REGION_EU_WEST_1)
s3_client.create_bucket(
Bucket="test-bucket",
CreateBucketConfiguration={"LocationConstraint": AWS_REGION_EU_WEST_1},
)
# Generate Firehose client
firehose_client = client("firehose", region_name=AWS_REGION_EU_WEST_1)
delivery_stream = firehose_client.create_delivery_stream(
DeliveryStreamName="test-delivery-stream",
DeliveryStreamType="DirectPut",
S3DestinationConfiguration={
"RoleARN": "arn:aws:iam::012345678901:role/firehose-role",
"BucketARN": "arn:aws:s3:::test-bucket",
"Prefix": "",
"BufferingHints": {"IntervalInSeconds": 300, "SizeInMBs": 5},
"CompressionFormat": "UNCOMPRESSED",
},
Tags=[{"Key": "key", "Value": "value"}],
)
arn = delivery_stream["DeliveryStreamARN"]
stream_name = arn.split("/")[-1]
firehose_client.start_delivery_stream_encryption(
DeliveryStreamName=stream_name,
DeliveryStreamEncryptionConfigurationInput={
"KeyARN": f"arn:aws:kms:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:key/test-kms-key-id",
"KeyType": "CUSTOMER_MANAGED_CMK",
},
)
from prowler.providers.aws.services.firehose.firehose_service import Firehose
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.firehose.firehose_stream_encrypted_at_rest.firehose_stream_encrypted_at_rest.firehose_client",
new=Firehose(aws_provider),
):
# Test Check
from prowler.providers.aws.services.firehose.firehose_stream_encrypted_at_rest.firehose_stream_encrypted_at_rest import (
firehose_stream_encrypted_at_rest,
)
check = firehose_stream_encrypted_at_rest()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Firehose Stream {stream_name} does have at rest encryption enabled."
)
@mock_aws
def test_stream_kms_encryption_not_enabled(self):
# Generate Firehose client
firehose_client = client("firehose", region_name=AWS_REGION_EU_WEST_1)
delivery_stream = firehose_client.create_delivery_stream(
DeliveryStreamName="test-delivery-stream",
DeliveryStreamType="DirectPut",
S3DestinationConfiguration={
"RoleARN": "arn:aws:iam::012345678901:role/firehose-role",
"BucketARN": "arn:aws:s3:::test-bucket",
"Prefix": "",
"BufferingHints": {"IntervalInSeconds": 300, "SizeInMBs": 5},
"CompressionFormat": "UNCOMPRESSED",
},
Tags=[{"Key": "key", "Value": "value"}],
)
arn = delivery_stream["DeliveryStreamARN"]
stream_name = arn.split("/")[-1]
from prowler.providers.aws.services.firehose.firehose_service import Firehose
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.firehose.firehose_stream_encrypted_at_rest.firehose_stream_encrypted_at_rest.firehose_client",
new=Firehose(aws_provider),
):
# Test Check
from prowler.providers.aws.services.firehose.firehose_stream_encrypted_at_rest.firehose_stream_encrypted_at_rest import (
firehose_stream_encrypted_at_rest,
)
check = firehose_stream_encrypted_at_rest()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Firehose Stream {stream_name} does not have at rest encryption enabled."
)
@mock_aws
def test_stream_kms_encryption_disabled(self):
# Generate Firehose client
firehose_client = client("firehose", region_name=AWS_REGION_EU_WEST_1)
delivery_stream = firehose_client.create_delivery_stream(
DeliveryStreamName="test-delivery-stream",
DeliveryStreamType="DirectPut",
S3DestinationConfiguration={
"RoleARN": "arn:aws:iam::012345678901:role/firehose-role",
"BucketARN": "arn:aws:s3:::test-bucket",
"Prefix": "",
"BufferingHints": {"IntervalInSeconds": 300, "SizeInMBs": 5},
"CompressionFormat": "UNCOMPRESSED",
},
Tags=[{"Key": "key", "Value": "value"}],
)
arn = delivery_stream["DeliveryStreamARN"]
stream_name = arn.split("/")[-1]
firehose_client.start_delivery_stream_encryption(
DeliveryStreamName=stream_name,
DeliveryStreamEncryptionConfigurationInput={
"KeyARN": f"arn:aws:kms:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:key/test-kms-key-id",
"KeyType": "CUSTOMER_MANAGED_CMK",
},
)
firehose_client.stop_delivery_stream_encryption(DeliveryStreamName=stream_name)
from prowler.providers.aws.services.firehose.firehose_service import Firehose
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.firehose.firehose_stream_encrypted_at_rest.firehose_stream_encrypted_at_rest.firehose_client",
new=Firehose(aws_provider),
):
# Test Check
from prowler.providers.aws.services.firehose.firehose_stream_encrypted_at_rest.firehose_stream_encrypted_at_rest import (
firehose_stream_encrypted_at_rest,
)
check = firehose_stream_encrypted_at_rest()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Firehose Stream {stream_name} does not have at rest encryption enabled."
)
@@ -1,4 +1,3 @@
from re import search
from unittest import mock
from boto3 import client
@@ -39,11 +38,10 @@ class Test_iam_no_root_access_key_test:
check = iam_no_root_access_key()
result = check.execute()
# raise Exception
assert result[0].status == "PASS"
assert search(
"User <root_account> does not have access keys.",
result[0].status_extended,
assert (
result[0].status_extended
== "Root account does not have access keys."
)
assert result[0].resource_id == "<root_account>"
assert (
@@ -82,11 +80,10 @@ class Test_iam_no_root_access_key_test:
check = iam_no_root_access_key()
result = check.execute()
# raise Exception
assert result[0].status == "FAIL"
assert search(
"User <root_account> has one active access key.",
result[0].status_extended,
assert (
result[0].status_extended
== "Root account has one active access key."
)
assert result[0].resource_id == "<root_account>"
assert (
@@ -125,11 +122,10 @@ class Test_iam_no_root_access_key_test:
check = iam_no_root_access_key()
result = check.execute()
# raise Exception
assert result[0].status == "FAIL"
assert search(
"User <root_account> has one active access key.",
result[0].status_extended,
assert (
result[0].status_extended
== "Root account has one active access key."
)
assert result[0].resource_id == "<root_account>"
assert (
@@ -168,11 +164,10 @@ class Test_iam_no_root_access_key_test:
check = iam_no_root_access_key()
result = check.execute()
# raise Exception
assert result[0].status == "FAIL"
assert search(
"User <root_account> has two active access key.",
result[0].status_extended,
assert (
result[0].status_extended
== "Root account has two active access keys."
)
assert result[0].resource_id == "<root_account>"
assert (
@@ -0,0 +1,154 @@
from unittest import mock
import botocore
from boto3 import client
from moto import mock_aws
from tests.providers.aws.utils import (
AWS_ACCOUNT_ARN,
AWS_ACCOUNT_NUMBER,
AWS_REGION_US_EAST_1,
set_mocked_aws_provider,
)
# Original botocore _make_api_call function
orig = botocore.client.BaseClient._make_api_call
# Mocked botocore _make_api_call function
def mock_make_api_call_enabled(self, operation_name, kwarg):
if operation_name == "ListOrganizationsFeatures":
return {
"OrganizationId": "o-test",
"EnabledFeatures": ["RootSessions", "RootCredentialsManagement"],
}
# If we don't want to patch the API call
return orig(self, operation_name, kwarg)
def mock_make_api_call_disabled(self, operation_name, kwarg):
if operation_name == "ListOrganizationsFeatures":
return {"OrganizationId": "o-test", "EnabledFeatures": []}
# If we don't want to patch the API call
return orig(self, operation_name, kwarg)
class Test_iam_root_credentials_management_enabled_test:
@mock_aws
def test_no_organization(self):
from prowler.providers.aws.services.iam.iam_service import IAM
from prowler.providers.aws.services.organizations.organizations_service import (
Organizations,
)
aws_provider = set_mocked_aws_provider(
[AWS_REGION_US_EAST_1], create_default_organization=False
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.iam.iam_root_credentials_management_enabled.iam_root_credentials_management_enabled.iam_client",
new=IAM(aws_provider),
), mock.patch(
"prowler.providers.aws.services.iam.iam_root_credentials_management_enabled.iam_root_credentials_management_enabled.organizations_client",
new=Organizations(aws_provider),
):
from prowler.providers.aws.services.iam.iam_root_credentials_management_enabled.iam_root_credentials_management_enabled import (
iam_root_credentials_management_enabled,
)
check = iam_root_credentials_management_enabled()
result = check.execute()
assert len(result) == 0
@mock.patch(
"botocore.client.BaseClient._make_api_call", new=mock_make_api_call_enabled
)
@mock_aws
def test__root_credentials_management_enabled(self):
# Create Organization
conn = client("organizations")
conn.create_organization()
from prowler.providers.aws.services.iam.iam_service import IAM
from prowler.providers.aws.services.organizations.organizations_service import (
Organizations,
)
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.iam.iam_root_credentials_management_enabled.iam_root_credentials_management_enabled.iam_client",
new=IAM(aws_provider),
), mock.patch(
"prowler.providers.aws.services.iam.iam_root_credentials_management_enabled.iam_root_credentials_management_enabled.organizations_client",
new=Organizations(aws_provider),
):
from prowler.providers.aws.services.iam.iam_root_credentials_management_enabled.iam_root_credentials_management_enabled import (
iam_root_credentials_management_enabled,
)
check = iam_root_credentials_management_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Root credentials management is enabled."
)
assert result[0].resource_id == AWS_ACCOUNT_NUMBER
assert result[0].resource_arn == AWS_ACCOUNT_ARN
assert result[0].region == AWS_REGION_US_EAST_1
@mock.patch(
"botocore.client.BaseClient._make_api_call", new=mock_make_api_call_disabled
)
@mock_aws
def test__root_credentials_management_disabled(self):
# Create Organization
conn = client("organizations")
conn.create_organization()
from prowler.providers.aws.services.iam.iam_service import IAM
from prowler.providers.aws.services.organizations.organizations_service import (
Organizations,
)
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.iam.iam_root_credentials_management_enabled.iam_root_credentials_management_enabled.iam_client",
new=IAM(aws_provider),
), mock.patch(
"prowler.providers.aws.services.iam.iam_root_credentials_management_enabled.iam_root_credentials_management_enabled.organizations_client",
new=Organizations(aws_provider),
):
from prowler.providers.aws.services.iam.iam_root_credentials_management_enabled.iam_root_credentials_management_enabled import (
iam_root_credentials_management_enabled,
)
check = iam_root_credentials_management_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Root credentials management is not enabled."
)
assert result[0].resource_id == AWS_ACCOUNT_NUMBER
assert result[0].resource_arn == AWS_ACCOUNT_ARN
assert result[0].region == AWS_REGION_US_EAST_1
@@ -29,6 +29,7 @@ class Test_iam_root_hardware_mfa_enabled_test:
iam_client.audited_partition = "aws"
iam_client.region = AWS_REGION_US_EAST_1
iam_client.mfa_arn_template = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:mfa"
iam_client.organization_features = []
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
@@ -63,6 +64,7 @@ class Test_iam_root_hardware_mfa_enabled_test:
iam_client.audited_partition = "aws"
iam_client.region = AWS_REGION_US_EAST_1
iam_client.mfa_arn_template = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:mfa"
iam_client.organization_features = []
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
@@ -95,6 +97,7 @@ class Test_iam_root_hardware_mfa_enabled_test:
iam_client.audited_partition = "aws"
iam_client.region = AWS_REGION_US_EAST_1
iam_client.mfa_arn_template = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:mfa"
iam_client.organization_features = []
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
@@ -1923,6 +1923,22 @@ class Test_Policy:
}
assert not is_policy_public(policy, TRUSTED_AWS_ACCOUNT_NUMBER)
def test_is_policy_public_eks(
self,
):
policy = {
"Statement": [
{
"Sid": "test",
"Effect": "Allow",
"Principal": {"Service": "eks.amazonaws.com"},
"Action": "lambda:GetFunction",
"Resource": "*",
}
]
}
assert not is_policy_public(policy, TRUSTED_AWS_ACCOUNT_NUMBER)
def test_is_policy_public_cross_cross_service_confused_deputy(
self,
):
@@ -3,7 +3,11 @@ from unittest.mock import patch
import botocore
from moto import mock_aws
from prowler.providers.aws.services.kinesis.kinesis_service import Kinesis, StreamStatus
from prowler.providers.aws.services.kinesis.kinesis_service import (
EncryptionType,
Kinesis,
StreamStatus,
)
from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider
make_api_call = botocore.client.BaseClient._make_api_call
@@ -28,6 +32,8 @@ def mock_make_api_call(self, operation_name, kwarg):
"StreamARN": "arn:aws:kinesis:us-east-1:123456789012:stream/test-stream",
"StreamStatus": "ACTIVE",
"Tags": [{"Key": "test_tag", "Value": "test_value"}],
"EncryptionType": "KMS",
"RetentionPeriodHours": 24,
}
}
if operation_name == "ListTagsForStream":
@@ -72,3 +78,17 @@ class Test_Kinesis_Service:
assert kinesis.streams[arn].tags == [{"Key": "test_tag", "Value": "test_value"}]
assert kinesis.streams[arn].region == AWS_REGION_US_EAST_1
assert kinesis.streams[arn].arn == arn
@mock_aws
def test_describe_stream(self):
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
kinesis = Kinesis(aws_provider)
arn = "arn:aws:kinesis:us-east-1:123456789012:stream/test-stream"
assert kinesis.streams[arn].name == "test-stream"
assert kinesis.streams[arn].status == StreamStatus.ACTIVE
assert kinesis.streams[arn].tags == [{"Key": "test_tag", "Value": "test_value"}]
assert kinesis.streams[arn].region == AWS_REGION_US_EAST_1
assert kinesis.streams[arn].arn == arn
assert kinesis.streams[arn].encrypted_at_rest == EncryptionType.KMS
assert kinesis.streams[arn].retention_period == 24
@@ -0,0 +1,147 @@
from unittest import mock
from boto3 import client
from moto import mock_aws
from tests.providers.aws.utils import (
AWS_ACCOUNT_NUMBER,
AWS_REGION_US_EAST_1,
set_mocked_aws_provider,
)
class Test_kinesis_encrypted_at_rest:
@mock_aws
def test_no_streams(self):
from prowler.providers.aws.services.kinesis.kinesis_service import Kinesis
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), mock.patch(
"prowler.providers.aws.services.kinesis.kinesis_stream_data_retention_period.kinesis_stream_data_retention_period.kinesis_client",
new=Kinesis(aws_provider),
):
# Test Check
from prowler.providers.aws.services.kinesis.kinesis_stream_data_retention_period.kinesis_stream_data_retention_period import (
kinesis_stream_data_retention_period,
)
check = kinesis_stream_data_retention_period()
result = check.execute()
assert len(result) == 0
@mock_aws
def test_adequate_retention_period(self):
kinesis_client = client("kinesis", region_name=AWS_REGION_US_EAST_1)
stream_name = "stream_test_us"
kinesis_client.create_stream(
StreamName=stream_name,
ShardCount=1,
StreamModeDetails={"StreamMode": "PROVISIONED"},
)
retention_period = 400
kinesis_client.increase_stream_retention_period(
StreamName=stream_name,
RetentionPeriodHours=retention_period,
StreamARN=f"arn:aws:kinesis:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:stream/{stream_name}",
)
kinesis_client.audit_config = mock.MagicMock()
kinesis_client.audit_config = {"min_kinesis_stream_retention_hours": 350}
from prowler.providers.aws.services.kinesis.kinesis_service import Kinesis
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), mock.patch(
"prowler.providers.aws.services.kinesis.kinesis_stream_data_retention_period.kinesis_stream_data_retention_period.kinesis_client",
new=Kinesis(aws_provider),
):
with mock.patch(
"prowler.providers.aws.services.kinesis.kinesis_stream_data_retention_period.kinesis_stream_data_retention_period.kinesis_client.audit_config",
new=kinesis_client.audit_config,
):
# Test Check
from prowler.providers.aws.services.kinesis.kinesis_stream_data_retention_period.kinesis_stream_data_retention_period import (
kinesis_stream_data_retention_period,
)
check = kinesis_stream_data_retention_period()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Kinesis Stream {stream_name} does have an adequate data retention period ({retention_period}hrs)."
)
assert result[0].resource_id == stream_name
assert (
result[0].resource_arn
== f"arn:aws:kinesis:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:stream/{stream_name}"
)
assert result[0].resource_tags == []
assert result[0].region == AWS_REGION_US_EAST_1
@mock_aws
def test_unadequate_retention_period(self):
kinesis_client = client("kinesis", region_name=AWS_REGION_US_EAST_1)
stream_name = "stream_test_us"
kinesis_client.create_stream(
StreamName=stream_name,
ShardCount=1,
StreamModeDetails={"StreamMode": "PROVISIONED"},
)
retention_period = 200
kinesis_client.increase_stream_retention_period(
StreamName=stream_name,
RetentionPeriodHours=retention_period,
StreamARN=f"arn:aws:kinesis:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:stream/{stream_name}",
)
kinesis_client.audit_config = mock.MagicMock()
kinesis_client.audit_config = {"min_kinesis_stream_retention_hours": 250}
from prowler.providers.aws.services.kinesis.kinesis_service import Kinesis
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), mock.patch(
"prowler.providers.aws.services.kinesis.kinesis_stream_data_retention_period.kinesis_stream_data_retention_period.kinesis_client",
new=Kinesis(aws_provider),
):
with mock.patch(
"prowler.providers.aws.services.kinesis.kinesis_stream_data_retention_period.kinesis_stream_data_retention_period.kinesis_client.audit_config",
new=kinesis_client.audit_config,
):
# Test Check
from prowler.providers.aws.services.kinesis.kinesis_stream_data_retention_period.kinesis_stream_data_retention_period import (
kinesis_stream_data_retention_period,
)
check = kinesis_stream_data_retention_period()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Kinesis Stream {stream_name} does not have an adequate data retention period ({retention_period}hrs)."
)
assert result[0].resource_id == stream_name
assert (
result[0].resource_arn
== f"arn:aws:kinesis:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:stream/{stream_name}"
)
assert result[0].resource_tags == []
assert result[0].region == AWS_REGION_US_EAST_1
@@ -15,14 +15,12 @@ class Test_organizations_tags_policies_enabled_and_attached:
def test_organization_no_organization(self):
organizations_client = mock.MagicMock
organizations_client.region = AWS_REGION_EU_WEST_1
organizations_client.organizations = [
Organization(
arn=AWS_ACCOUNT_ARN,
id="AWS Organization",
status="NOT_AVAILABLE",
master_id="",
)
]
organizations_client.organization = Organization(
arn=AWS_ACCOUNT_ARN,
id="AWS Organization",
status="NOT_AVAILABLE",
master_id="",
)
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
@@ -55,16 +53,14 @@ class Test_organizations_tags_policies_enabled_and_attached:
def test_organization_with_AI_optout_no_policies(self):
organizations_client = mock.MagicMock
organizations_client.region = AWS_REGION_EU_WEST_1
organizations_client.organizations = [
Organization(
id="o-1234567890",
arn="arn:aws:organizations::1234567890:organization/o-1234567890",
status="ACTIVE",
master_id="1234567890",
policies={"AISERVICES_OPT_OUT_POLICY": []},
delegated_administrators=None,
)
]
organizations_client.organization = Organization(
id="o-1234567890",
arn="arn:aws:organizations::1234567890:organization/o-1234567890",
status="ACTIVE",
master_id="1234567890",
policies={"AISERVICES_OPT_OUT_POLICY": []},
delegated_administrators=None,
)
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
@@ -100,33 +96,29 @@ class Test_organizations_tags_policies_enabled_and_attached:
def test_organization_with_AI_optout_policy(self):
organizations_client = mock.MagicMock
organizations_client.region = AWS_REGION_EU_WEST_1
organizations_client.organizations = [
Organization(
id="o-1234567890",
arn="arn:aws:organizations::1234567890:organization/o-1234567890",
status="ACTIVE",
master_id="1234567890",
policies={
"AISERVICES_OPT_OUT_POLICY": [
Policy(
id="p-1234567890",
arn="arn:aws:organizations::1234567890:policy/o-1234567890/p-1234567890",
type="AISERVICES_OPT_OUT_POLICY",
aws_managed=False,
content={
"services": {
"default": {
"opt_out_policy": {"@@assign": "optOut"}
}
}
},
targets=[],
)
]
},
delegated_administrators=None,
)
]
organizations_client.organization = Organization(
id="o-1234567890",
arn="arn:aws:organizations::1234567890:organization/o-1234567890",
status="ACTIVE",
master_id="1234567890",
policies={
"AISERVICES_OPT_OUT_POLICY": [
Policy(
id="p-1234567890",
arn="arn:aws:organizations::1234567890:policy/o-1234567890/p-1234567890",
type="AISERVICES_OPT_OUT_POLICY",
aws_managed=False,
content={
"services": {
"default": {"opt_out_policy": {"@@assign": "optOut"}}
}
},
targets=[],
)
]
},
delegated_administrators=None,
)
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
@@ -162,27 +154,25 @@ class Test_organizations_tags_policies_enabled_and_attached:
def test_organization_with_AI_optout_policy_no_content(self):
organizations_client = mock.MagicMock
organizations_client.region = AWS_REGION_EU_WEST_1
organizations_client.organizations = [
Organization(
id="o-1234567890",
arn="arn:aws:organizations::1234567890:organization/o-1234567890",
status="ACTIVE",
master_id="1234567890",
policies={
"AISERVICES_OPT_OUT_POLICY": [
Policy(
id="p-1234567890",
arn="arn:aws:organizations::1234567890:policy/o-1234567890/p-1234567890",
type="AISERVICES_OPT_OUT_POLICY",
aws_managed=False,
content={},
targets=[],
)
]
},
delegated_administrators=None,
)
]
organizations_client.organization = Organization(
id="o-1234567890",
arn="arn:aws:organizations::1234567890:organization/o-1234567890",
status="ACTIVE",
master_id="1234567890",
policies={
"AISERVICES_OPT_OUT_POLICY": [
Policy(
id="p-1234567890",
arn="arn:aws:organizations::1234567890:policy/o-1234567890/p-1234567890",
type="AISERVICES_OPT_OUT_POLICY",
aws_managed=False,
content={},
targets=[],
)
]
},
delegated_administrators=None,
)
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
@@ -247,7 +247,7 @@ class Test_organizations_scp_check_deny_regions:
# Create Organization
conn = client("organizations", region_name=AWS_REGION_EU_WEST_1)
response = conn.create_organization()
org_arn = response["Organization"]["Arn"]
response["Organization"]["Arn"]
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
@@ -262,9 +262,7 @@ class Test_organizations_scp_check_deny_regions:
organizations_scp_check_deny_regions,
)
for org in organizations_client.organizations:
if org.arn == org_arn:
org.policies = None
organizations_client.organization.policies = None
check = organizations_scp_check_deny_regions()
result = check.execute()
@@ -28,15 +28,14 @@ class Test_Organizations_Service:
[AWS_REGION_EU_WEST_1], create_default_organization=False
)
organizations = Organizations(aws_provider)
assert len(organizations.organizations) == 1
assert organizations.organizations[0].arn == response["Organization"]["Arn"]
assert organizations.organizations[0].id == response["Organization"]["Id"]
assert organizations.organization.arn == response["Organization"]["Arn"]
assert organizations.organization.id == response["Organization"]["Id"]
assert (
organizations.organizations[0].master_id
organizations.organization.master_id
== response["Organization"]["MasterAccountId"]
)
assert organizations.organizations[0].status == "ACTIVE"
assert organizations.organizations[0].delegated_administrators == []
assert organizations.organization.status == "ACTIVE"
assert organizations.organization.delegated_administrators == []
@mock_aws
def test_list_policies(self):
@@ -15,14 +15,12 @@ class Test_organizations_tags_policies_enabled_and_attached:
def test_organization_no_organization(self):
organizations_client = mock.MagicMock
organizations_client.region = AWS_REGION_EU_WEST_1
organizations_client.organizations = [
Organization(
arn=AWS_ACCOUNT_ARN,
id="AWS Organization",
status="NOT_AVAILABLE",
master_id="",
)
]
organizations_client.organization = Organization(
arn=AWS_ACCOUNT_ARN,
id="AWS Organization",
status="NOT_AVAILABLE",
master_id="",
)
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
@@ -55,27 +53,25 @@ class Test_organizations_tags_policies_enabled_and_attached:
def test_organization_with_tag_policies_not_attached(self):
organizations_client = mock.MagicMock
organizations_client.region = AWS_REGION_EU_WEST_1
organizations_client.organizations = [
Organization(
id="o-1234567890",
arn="arn:aws:organizations::1234567890:organization/o-1234567890",
status="ACTIVE",
master_id="1234567890",
policies={
"TAG_POLICY": [
Policy(
id="p-1234567890",
arn="arn:aws:organizations::1234567890:policy/o-1234567890/p-1234567890",
type="TAG_POLICY",
aws_managed=False,
content={"tags": {"Owner": {}}},
targets=[],
)
]
},
delegated_administrators=None,
)
]
organizations_client.organization = Organization(
id="o-1234567890",
arn="arn:aws:organizations::1234567890:organization/o-1234567890",
status="ACTIVE",
master_id="1234567890",
policies={
"TAG_POLICY": [
Policy(
id="p-1234567890",
arn="arn:aws:organizations::1234567890:policy/o-1234567890/p-1234567890",
type="TAG_POLICY",
aws_managed=False,
content={"tags": {"Owner": {}}},
targets=[],
)
]
},
delegated_administrators=None,
)
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
@@ -111,27 +107,25 @@ class Test_organizations_tags_policies_enabled_and_attached:
def test_organization_with_tag_policies_attached(self):
organizations_client = mock.MagicMock
organizations_client.region = AWS_REGION_EU_WEST_1
organizations_client.organizations = [
Organization(
id="o-1234567890",
arn="arn:aws:organizations::1234567890:organization/o-1234567890",
status="ACTIVE",
master_id="1234567890",
policies={
"TAG_POLICY": [
Policy(
id="p-1234567890",
arn="arn:aws:organizations::1234567890:policy/o-1234567890/p-1234567890",
type="TAG_POLICY",
aws_managed=False,
content={"tags": {"Owner": {}}},
targets=["1234567890"],
)
]
},
delegated_administrators=None,
)
]
organizations_client.organization = Organization(
id="o-1234567890",
arn="arn:aws:organizations::1234567890:organization/o-1234567890",
status="ACTIVE",
master_id="1234567890",
policies={
"TAG_POLICY": [
Policy(
id="p-1234567890",
arn="arn:aws:organizations::1234567890:policy/o-1234567890/p-1234567890",
type="TAG_POLICY",
aws_managed=False,
content={"tags": {"Owner": {}}},
targets=["1234567890"],
)
]
},
delegated_administrators=None,
)
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
@@ -0,0 +1,187 @@
from unittest import mock
from boto3 import client
from moto import mock_aws
from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider
class Test_rds_snapshots_public_access_fixer:
@mock_aws
def test_rds_private_snapshot(self):
conn = client("rds", region_name=AWS_REGION_US_EAST_1)
conn.create_db_instance(
DBInstanceIdentifier="db-primary-1",
AllocatedStorage=10,
Engine="postgres",
DBName="staging-postgres",
DBInstanceClass="db.m1.small",
)
conn.create_db_snapshot(
DBInstanceIdentifier="db-primary-1", DBSnapshotIdentifier="snapshot-1"
)
from prowler.providers.aws.services.rds.rds_service import RDS
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.rds.rds_snapshots_public_access.rds_snapshots_public_access_fixer.rds_client",
new=RDS(aws_provider),
):
# Test Fixer
from prowler.providers.aws.services.rds.rds_snapshots_public_access.rds_snapshots_public_access_fixer import (
fixer,
)
assert fixer("snapshot-1", AWS_REGION_US_EAST_1)
@mock_aws
def test_rds_public_snapshot(self):
conn = client("rds", region_name=AWS_REGION_US_EAST_1)
conn.create_db_instance(
DBInstanceIdentifier="db-primary-1",
AllocatedStorage=10,
Engine="postgres",
DBName="staging-postgres",
DBInstanceClass="db.m1.small",
)
conn.create_db_snapshot(
DBInstanceIdentifier="db-primary-1", DBSnapshotIdentifier="snapshot-1"
)
from prowler.providers.aws.services.rds.rds_service import RDS
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.rds.rds_snapshots_public_access.rds_snapshots_public_access_fixer.rds_client",
new=RDS(aws_provider),
) as service_client:
service_client.db_snapshots[0].public = True
# Test Fixer
from prowler.providers.aws.services.rds.rds_snapshots_public_access.rds_snapshots_public_access_fixer import (
fixer,
)
assert fixer("snapshot-1", AWS_REGION_US_EAST_1)
@mock_aws
def test_rds_cluster_private_snapshot(self):
conn = client("rds", region_name=AWS_REGION_US_EAST_1)
conn.create_db_cluster(
DBClusterIdentifier="db-primary-1",
AllocatedStorage=10,
Engine="postgres",
DBClusterInstanceClass="db.m1.small",
MasterUsername="root",
MasterUserPassword="hunter2000",
)
conn.create_db_cluster_snapshot(
DBClusterIdentifier="db-primary-1", DBClusterSnapshotIdentifier="snapshot-1"
)
from prowler.providers.aws.services.rds.rds_service import RDS
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.rds.rds_snapshots_public_access.rds_snapshots_public_access_fixer.rds_client",
new=RDS(aws_provider),
):
# Test Fixer
from prowler.providers.aws.services.rds.rds_snapshots_public_access.rds_snapshots_public_access_fixer import (
fixer,
)
assert fixer("snapshot-1", AWS_REGION_US_EAST_1)
@mock_aws
def test_rds_cluster_public_snapshot(self):
conn = client("rds", region_name=AWS_REGION_US_EAST_1)
conn.create_db_cluster(
DBClusterIdentifier="db-primary-1",
AllocatedStorage=10,
Engine="postgres",
DBClusterInstanceClass="db.m1.small",
MasterUsername="root",
MasterUserPassword="hunter2000",
)
conn.create_db_cluster_snapshot(
DBClusterIdentifier="db-primary-1", DBClusterSnapshotIdentifier="snapshot-1"
)
from prowler.providers.aws.services.rds.rds_service import RDS
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.rds.rds_snapshots_public_access.rds_snapshots_public_access_fixer.rds_client",
new=RDS(aws_provider),
) as service_client:
service_client.db_cluster_snapshots[0].public = True
# Test Fixer
from prowler.providers.aws.services.rds.rds_snapshots_public_access.rds_snapshots_public_access_fixer import (
fixer,
)
assert fixer("snapshot-1", AWS_REGION_US_EAST_1)
@mock_aws
def test_rds_cluster_public_snapshot_error(self):
conn = client("rds", region_name=AWS_REGION_US_EAST_1)
conn.create_db_cluster(
DBClusterIdentifier="db-primary-1",
AllocatedStorage=10,
Engine="postgres",
DBClusterInstanceClass="db.m1.small",
MasterUsername="root",
MasterUserPassword="hunter2000",
)
conn.create_db_cluster_snapshot(
DBClusterIdentifier="db-primary-1", DBClusterSnapshotIdentifier="snapshot-1"
)
from prowler.providers.aws.services.rds.rds_service import RDS
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.rds.rds_snapshots_public_access.rds_snapshots_public_access_fixer.rds_client",
new=RDS(aws_provider),
) as service_client:
service_client.db_cluster_snapshots[0].public = True
# Test Fixer
from prowler.providers.aws.services.rds.rds_snapshots_public_access.rds_snapshots_public_access_fixer import (
fixer,
)
assert not fixer("snapshot-2", AWS_REGION_US_EAST_1)
@@ -439,3 +439,34 @@ class TestAzureProvider:
assert exception.type == Exception
assert exception.value.args[0] == "Simulated Exception"
@pytest.mark.parametrize(
"subscription_ids, expected_regions",
[
(None, {"region1", "region2", "region3"}),
(["sub1", "sub2"], {"region1", "region2", "region3"}),
("sub1", {"region1", "region2"}),
("not_exists", set()),
],
)
@patch("prowler.providers.azure.azure_provider.AzureProvider.get_locations")
@patch(
"prowler.providers.azure.azure_provider.AzureProvider.__init__",
return_value=None,
)
def test_get_regions(
self,
azure_provider_init_mock, # noqa: F841
azure_get_locations_mock,
subscription_ids,
expected_regions,
):
azure_get_locations_mock.return_value = {
"sub1": ["region1", "region2"],
"sub2": ["region2", "region3"],
}
azure_provider = AzureProvider()
regions = azure_provider.get_regions(subscription_ids=subscription_ids)
assert regions == expected_regions
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff. Show more