mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-15 07:54:21 +00:00
Merge branch 'master' into github-poc
This commit is contained in:
115 files changed
+3868
-1830
No files matched your search
@@ -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/**"
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
@@ -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 && \
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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).
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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,
|
||||
|
||||
@@ -38,7 +38,7 @@ class AzureCISModel(BaseModel):
|
||||
|
||||
Provider: str
|
||||
Description: str
|
||||
Subscription: str
|
||||
SubscriptionId: str
|
||||
Location: str
|
||||
AssessmentDate: str
|
||||
Requirements_Id: str
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+3
-1
@@ -25,7 +25,9 @@
|
||||
"Url": "https://aws.amazon.com/blogs/security/how-to-get-ready-for-certificate-transparency/"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Categories": [
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
+2
-1
@@ -29,7 +29,8 @@
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"forensics-ready"
|
||||
"forensics-ready",
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
|
||||
+3
-1
@@ -28,7 +28,9 @@
|
||||
"Url": "https://docs.aws.amazon.com/apigateway/latest/developerguide/security-monitoring.html"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Categories": [
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
+2
-1
@@ -24,7 +24,8 @@
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"forensics-ready"
|
||||
"forensics-ready",
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
|
||||
+2
-1
@@ -24,7 +24,8 @@
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"forensics-ready"
|
||||
"forensics-ready",
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
|
||||
+2
-1
@@ -28,7 +28,8 @@
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"forensics-ready"
|
||||
"forensics-ready",
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
|
||||
-32
@@ -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": ""
|
||||
}
|
||||
-147
@@ -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.
|
||||
|
||||
+3
-1
@@ -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"),
|
||||
|
||||
+2
-1
@@ -26,7 +26,8 @@
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"forensics-ready"
|
||||
"forensics-ready",
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
|
||||
+2
-1
@@ -26,7 +26,8 @@
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"forensics-ready"
|
||||
"forensics-ready",
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
|
||||
+4
-1
@@ -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": ""
|
||||
|
||||
File renamed without changes.
@@ -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)
|
||||
Whitespace-only changes.
+32
@@ -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": ""
|
||||
}
|
||||
+43
@@ -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
|
||||
+3
-1
@@ -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."
|
||||
|
||||
+34
-30
@@ -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
|
||||
+1
-1
@@ -28,5 +28,5 @@
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "CAF Security Epic: IAM"
|
||||
"Notes": ""
|
||||
}
|
||||
Whitespace-only changes.
+34
@@ -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."
|
||||
}
|
||||
+28
@@ -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
|
||||
+31
-23
@@ -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
|
||||
Whitespace-only changes.
+32
@@ -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": ""
|
||||
}
|
||||
+37
@@ -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
|
||||
+3
-1
@@ -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())
|
||||
|
||||
+3
-1
@@ -25,7 +25,9 @@
|
||||
"Url": "https://docs.aws.amazon.com/neptune/latest/userguide/cloudwatch-logs.html"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Categories": [
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
+2
-1
@@ -27,7 +27,8 @@
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"forensics-ready"
|
||||
"forensics-ready",
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
|
||||
+2
-1
@@ -27,7 +27,8 @@
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"forensics-ready"
|
||||
"forensics-ready",
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
|
||||
+5
-7
@@ -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
|
||||
+18
-16
@@ -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)
|
||||
|
||||
|
||||
+12
-8
@@ -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)
|
||||
|
||||
+16
-14
@@ -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:
|
||||
|
||||
+13
-11
@@ -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)
|
||||
|
||||
|
||||
+3
-1
@@ -23,7 +23,9 @@
|
||||
"Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/publishing_cloudwatchlogs.html"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Categories": [
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
+3
-1
@@ -23,7 +23,9 @@
|
||||
"Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/publishing_cloudwatchlogs.html"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Categories": [
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
+63
@@ -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
|
||||
+2
-1
@@ -24,7 +24,8 @@
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"forensics-ready"
|
||||
"forensics-ready",
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
|
||||
+19
-17
@@ -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
|
||||
+2
-1
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
+26
-6
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
-206
@@ -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"
|
||||
)
|
||||
+195
@@ -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."
|
||||
)
|
||||
+12
-17
@@ -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 (
|
||||
|
||||
+154
@@ -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
|
||||
+3
@@ -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
|
||||
+147
@@ -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
|
||||
+56
-66
@@ -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])
|
||||
|
||||
|
||||
+2
-4
@@ -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):
|
||||
|
||||
+44
-50
@@ -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])
|
||||
|
||||
|
||||
+187
@@ -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
Reference in new issue
Block a user