diff --git a/docs/tutorials/configuration_file.md b/docs/tutorials/configuration_file.md index 9fa26d55f46..53e433d2c7e 100644 --- a/docs/tutorials/configuration_file.md +++ b/docs/tutorials/configuration_file.md @@ -34,6 +34,8 @@ The following list includes all the AWS checks with configurable variables that | `guardduty_is_enabled` | `allowlist_non_default_regions` | Boolean | | `securityhub_enabled` | `allowlist_non_default_regions` | Boolean | | `rds_instance_backup_enabled` | `check_rds_instance_replicas` | Boolean | +| `acm_certificates_expiration_check` | `days_to_expire_threshold` | Integer | + ## Azure ### Configurable Checks @@ -59,7 +61,6 @@ The following list includes all the Azure checks with configurable variables tha ```yaml title="config.yaml" # AWS Configuration aws: - # AWS Global Configuration # aws.allowlist_non_default_regions --> Allowlist Failed Findings in non-default regions for GuardDuty, SecurityHub, DRS and Config allowlist_non_default_regions: False @@ -72,6 +73,7 @@ aws: # AWS EC2 Configuration # aws.ec2_elastic_ip_shodan + # TODO: create common config shodan_api_key: null # aws.ec2_securitygroup_with_many_ingress_egress_rules --> by default is 50 rules max_security_group_rules: 50 @@ -124,24 +126,24 @@ aws: ] # AWS Organizations - # organizations_scp_check_deny_regions - # organizations_enabled_regions: [ - # 'eu-central-1', - # 'eu-west-1', + # aws.organizations_scp_check_deny_regions + # aws.organizations_enabled_regions: [ + # "eu-central-1", + # "eu-west-1", # "us-east-1" # ] organizations_enabled_regions: [] organizations_trusted_delegated_administrators: [] # AWS ECR - # ecr_repositories_scan_vulnerabilities_in_latest_image + # aws.ecr_repositories_scan_vulnerabilities_in_latest_image # CRITICAL # HIGH # MEDIUM ecr_repository_vulnerability_minimum_severity: "MEDIUM" # AWS Trusted Advisor - # trustedadvisor_premium_support_plan_subscribed + # aws.trustedadvisor_premium_support_plan_subscribed verify_premium_support_plans: True # AWS RDS @@ -149,13 +151,18 @@ aws: # Whether to check RDS instance replicas or not check_rds_instance_replicas: False + # AWS ACM Configuration + # aws.acm_certificates_expiration_check + days_to_expire_threshold: 7 + # Azure Configuration azure: # Azure Network Configuration # azure.network_public_ip_shodan + # TODO: create common config shodan_api_key: null - # Azure App Configuration + # Azure App Service # azure.app_ensure_php_version_is_latest php_latest_version: "8.2" # azure.app_ensure_python_version_is_latest diff --git a/docs/tutorials/ignore-unused-services.md b/docs/tutorials/ignore-unused-services.md index 8c7e22180d8..163d13d0eeb 100644 --- a/docs/tutorials/ignore-unused-services.md +++ b/docs/tutorials/ignore-unused-services.md @@ -11,6 +11,12 @@ prowler --ignore-unused-services ## Services that can be ignored ### AWS +#### ACM +You can have certificates in ACM that is not in use by any AWS resource. +Prowler will check if every certificate is going to expire soon, if this certificate is not in use by default it is not going to be check if it is expired, is going to expire soon or it is good. + +- `acm_certificates_expiration_check` + #### Athena When you create an AWS Account, Athena will create a default primary workgroup for you. Prowler will check if that workgroup is enabled and if it is being used by checking if there were queries in the last 45 days. diff --git a/prowler/config/config.yaml b/prowler/config/config.yaml index 7d875ac6968..05161daf964 100644 --- a/prowler/config/config.yaml +++ b/prowler/config/config.yaml @@ -101,6 +101,10 @@ aws: # Whether to check RDS instance replicas or not check_rds_instance_replicas: False + # AWS ACM Configuration + # aws.acm_certificates_expiration_check + days_to_expire_threshold: 7 + # Azure Configuration azure: # Azure Network Configuration diff --git a/prowler/lib/check/compliance.py b/prowler/lib/check/compliance.py index 21932406cbd..b191f1f8a3a 100644 --- a/prowler/lib/check/compliance.py +++ b/prowler/lib/check/compliance.py @@ -34,6 +34,7 @@ def update_checks_metadata_with_compliance( # Save it into the check's metadata bulk_checks_metadata[check].Compliance = check_compliance + check_compliance = [] # Add requirements of Manual Controls for framework in bulk_compliance_frameworks.values(): for requirement in framework.Requirements: @@ -70,7 +71,6 @@ def update_checks_metadata_with_compliance( "Recommendation": {"Text": "", "Url": ""}, }, "Categories": [], - "Tags": {}, "DependsOn": [], "RelatedTo": [], "Notes": "", diff --git a/prowler/lib/outputs/outputs.py b/prowler/lib/outputs/outputs.py index b81861674c8..1e1d092f0ba 100644 --- a/prowler/lib/outputs/outputs.py +++ b/prowler/lib/outputs/outputs.py @@ -67,15 +67,15 @@ def report(check_findings, output_options, audit_info): compliance in output_options.output_modes for compliance in available_compliance_frameworks ): - fill_compliance( + add_manual_controls( output_options, - finding, audit_info, file_descriptors, ) - add_manual_controls( + fill_compliance( output_options, + finding, audit_info, file_descriptors, ) diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index 8b638a2105b..64998767814 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -157,7 +157,10 @@ "us-west-1", "us-west-2" ], - "aws-cn": [], + "aws-cn": [ + "cn-north-1", + "cn-northwest-1" + ], "aws-us-gov": [ "us-gov-east-1", "us-gov-west-1" @@ -395,15 +398,22 @@ "ap-northeast-2", "ap-northeast-3", "ap-south-1", + "ap-south-2", "ap-southeast-1", "ap-southeast-2", + "ap-southeast-3", + "ap-southeast-4", "ca-central-1", + "ca-west-1", "eu-central-1", + "eu-central-2", "eu-north-1", "eu-south-1", + "eu-south-2", "eu-west-1", "eu-west-2", "eu-west-3", + "il-central-1", "me-central-1", "me-south-1", "sa-east-1", @@ -751,6 +761,18 @@ "aws-us-gov": [] } }, + "apptest": { + "regions": { + "aws": [ + "ap-southeast-2", + "eu-central-1", + "sa-east-1", + "us-east-1" + ], + "aws-cn": [], + "aws-us-gov": [] + } + }, "aps": { "regions": { "aws": [ @@ -1165,7 +1187,7 @@ ] } }, - "backupstorage": { + "batch": { "regions": { "aws": [ "af-south-1", @@ -1180,6 +1202,7 @@ "ap-southeast-3", "ap-southeast-4", "ca-central-1", + "ca-west-1", "eu-central-1", "eu-central-2", "eu-north-1", @@ -1188,6 +1211,7 @@ "eu-west-1", "eu-west-2", "eu-west-3", + "il-central-1", "me-central-1", "me-south-1", "sa-east-1", @@ -1206,47 +1230,13 @@ ] } }, - "batch": { + "bcm-data-exports": { "regions": { "aws": [ - "af-south-1", - "ap-east-1", - "ap-northeast-1", - "ap-northeast-2", - "ap-northeast-3", - "ap-south-1", - "ap-south-2", - "ap-southeast-1", - "ap-southeast-2", - "ap-southeast-3", - "ap-southeast-4", - "ca-central-1", - "ca-west-1", - "eu-central-1", - "eu-central-2", - "eu-north-1", - "eu-south-1", - "eu-south-2", - "eu-west-1", - "eu-west-2", - "eu-west-3", - "il-central-1", - "me-central-1", - "me-south-1", - "sa-east-1", - "us-east-1", - "us-east-2", - "us-west-1", - "us-west-2" - ], - "aws-cn": [ - "cn-north-1", - "cn-northwest-1" + "us-east-1" ], - "aws-us-gov": [ - "us-gov-east-1", - "us-gov-west-1" - ] + "aws-cn": [], + "aws-us-gov": [] } }, "bedrock": { @@ -1256,9 +1246,12 @@ "ap-south-1", "ap-southeast-1", "ap-southeast-2", + "ca-central-1", "eu-central-1", "eu-west-1", + "eu-west-2", "eu-west-3", + "sa-east-1", "us-east-1", "us-west-2" ], @@ -1442,6 +1435,7 @@ "chime-sdk-meetings": { "regions": { "aws": [ + "af-south-1", "ap-northeast-1", "ap-northeast-2", "ap-south-1", @@ -2577,6 +2571,8 @@ "connectcases": { "regions": { "aws": [ + "ap-northeast-1", + "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ca-central-1", @@ -2609,6 +2605,46 @@ ] } }, + "controlcatalog": { + "regions": { + "aws": [ + "af-south-1", + "ap-east-1", + "ap-northeast-1", + "ap-northeast-2", + "ap-northeast-3", + "ap-south-1", + "ap-south-2", + "ap-southeast-1", + "ap-southeast-2", + "ap-southeast-3", + "ap-southeast-4", + "ca-central-1", + "ca-west-1", + "eu-central-1", + "eu-central-2", + "eu-north-1", + "eu-south-1", + "eu-south-2", + "eu-west-1", + "eu-west-2", + "eu-west-3", + "il-central-1", + "me-central-1", + "me-south-1", + "sa-east-1", + "us-east-1", + "us-east-2", + "us-west-1", + "us-west-2" + ], + "aws-cn": [], + "aws-us-gov": [ + "us-gov-east-1", + "us-gov-west-1" + ] + } + }, "controltower": { "regions": { "aws": [ @@ -5788,7 +5824,10 @@ "aws-cn": [ "cn-north-1" ], - "aws-us-gov": [] + "aws-us-gov": [ + "us-gov-east-1", + "us-gov-west-1" + ] } }, "kms": { @@ -7037,6 +7076,7 @@ "ap-southeast-3", "ap-southeast-4", "ca-central-1", + "ca-west-1", "eu-central-1", "eu-central-2", "eu-north-1", @@ -7292,6 +7332,7 @@ "ap-southeast-3", "ap-southeast-4", "ca-central-1", + "ca-west-1", "eu-central-1", "eu-central-2", "eu-north-1", @@ -7418,10 +7459,12 @@ "ap-south-1", "ap-southeast-1", "ap-southeast-2", + "ca-central-1", "eu-central-1", "eu-west-1", "eu-west-2", "eu-west-3", + "sa-east-1", "us-east-1", "us-east-2", "us-west-2" @@ -8305,6 +8348,7 @@ "eu-central-2", "eu-south-2", "eu-west-3", + "me-central-1", "sa-east-1", "us-east-1", "us-east-2", @@ -9273,7 +9317,10 @@ "us-west-2" ], "aws-cn": [], - "aws-us-gov": [] + "aws-us-gov": [ + "us-gov-east-1", + "us-gov-west-1" + ] } }, "serverlessrepo": { @@ -10228,6 +10275,15 @@ ] } }, + "taxsettings": { + "regions": { + "aws": [ + "us-east-1" + ], + "aws-cn": [], + "aws-us-gov": [] + } + }, "textract": { "regions": { "aws": [ @@ -11095,4 +11151,4 @@ } } } -} \ No newline at end of file +} diff --git a/prowler/providers/aws/lib/policy_condition_parser/policy_condition_parser.py b/prowler/providers/aws/lib/policy_condition_parser/policy_condition_parser.py index 7dde2bdd31d..7b86d7ec895 100644 --- a/prowler/providers/aws/lib/policy_condition_parser/policy_condition_parser.py +++ b/prowler/providers/aws/lib/policy_condition_parser/policy_condition_parser.py @@ -29,6 +29,8 @@ def is_condition_block_restrictive( "aws:principalaccount", "aws:resourceaccount", "aws:sourcearn", + "aws:sourcevpc", + "aws:sourcevpce", ], "StringLike": [ "aws:sourceaccount", @@ -37,6 +39,8 @@ def is_condition_block_restrictive( "aws:principalarn", "aws:resourceaccount", "aws:principalaccount", + "aws:sourcevpc", + "aws:sourcevpce", ], "ArnLike": ["aws:sourcearn", "aws:principalarn"], "ArnEquals": ["aws:sourcearn", "aws:principalarn"], diff --git a/prowler/providers/aws/services/acm/acm_certificates_expiration_check/acm_certificates_expiration_check.py b/prowler/providers/aws/services/acm/acm_certificates_expiration_check/acm_certificates_expiration_check.py index 0557c51317a..3cb5b7e398a 100644 --- a/prowler/providers/aws/services/acm/acm_certificates_expiration_check/acm_certificates_expiration_check.py +++ b/prowler/providers/aws/services/acm/acm_certificates_expiration_check/acm_certificates_expiration_check.py @@ -1,33 +1,36 @@ from prowler.lib.check.models import Check, Check_Report_AWS from prowler.providers.aws.services.acm.acm_client import acm_client -DAYS_TO_EXPIRE_THRESHOLD = 7 - class acm_certificates_expiration_check(Check): def execute(self): findings = [] for certificate in acm_client.certificates: - report = Check_Report_AWS(self.metadata()) - report.region = certificate.region - if certificate.expiration_days > DAYS_TO_EXPIRE_THRESHOLD: - report.status = "PASS" - report.status_extended = f"ACM Certificate {certificate.id} for {certificate.name} expires in {certificate.expiration_days} days." - report.resource_id = certificate.id - report.resource_details = certificate.name - report.resource_arn = certificate.arn - report.resource_tags = certificate.tags - else: - report.status = "FAIL" - if certificate.expiration_days < 0: - report.status_extended = f"ACM Certificate {certificate.id} for {certificate.name} has expired ({abs(certificate.expiration_days)} days ago)." + if certificate.in_use or acm_client.provider.scan_unused_services: + report = Check_Report_AWS(self.metadata()) + report.region = certificate.region + if certificate.expiration_days > acm_client.audit_config.get( + "days_to_expire_threshold", 7 + ): + report.status = "PASS" + report.status_extended = f"ACM Certificate {certificate.id} for {certificate.name} expires in {certificate.expiration_days} days." + report.resource_id = certificate.id + report.resource_details = certificate.name + report.resource_arn = certificate.arn + report.resource_tags = certificate.tags else: - report.status_extended = f"ACM Certificate {certificate.id} for {certificate.name} is about to expire in {certificate.expiration_days} days." + report.status = "FAIL" + if certificate.expiration_days < 0: + report.status_extended = f"ACM Certificate {certificate.id} for {certificate.name} has expired ({abs(certificate.expiration_days)} days ago)." + report.check_metadata.Severity = "high" + else: + report.status_extended = f"ACM Certificate {certificate.id} for {certificate.name} is about to expire in {certificate.expiration_days} days." + report.check_metadata.Severity = "medium" - report.resource_id = certificate.id - report.resource_details = certificate.name - report.resource_arn = certificate.arn - report.resource_tags = certificate.tags + report.resource_id = certificate.id + report.resource_details = certificate.name + report.resource_arn = certificate.arn + report.resource_tags = certificate.tags - findings.append(report) + findings.append(report) return findings diff --git a/prowler/providers/aws/services/acm/acm_service.py b/prowler/providers/aws/services/acm/acm_service.py index 4f0e5de42ed..e3fc8a59918 100644 --- a/prowler/providers/aws/services/acm/acm_service.py +++ b/prowler/providers/aws/services/acm/acm_service.py @@ -50,6 +50,7 @@ def __list_certificates__(self, regional_client): id=certificate["CertificateArn"].split("/")[-1], type=certificate["Type"], expiration_days=certificate_expiration_time, + in_use=certificate.get("InUse", False), transparency_logging=False, region=regional_client.region, ) @@ -99,5 +100,6 @@ class Certificate(BaseModel): type: str tags: Optional[list] = [] expiration_days: int + in_use: bool transparency_logging: Optional[bool] region: str diff --git a/prowler/providers/aws/services/cloudwatch/cloudwatch_service.py b/prowler/providers/aws/services/cloudwatch/cloudwatch_service.py index 2066ae0a6f1..8db4aac3265 100644 --- a/prowler/providers/aws/services/cloudwatch/cloudwatch_service.py +++ b/prowler/providers/aws/services/cloudwatch/cloudwatch_service.py @@ -95,7 +95,7 @@ def __init__(self, audit_info): 1000 # The threshold for number of events to return per log group. ) self.__threading_call__(self.__get_log_events__) - self.__list_tags_for_resource__() + self.__threading_call__(self.__list_tags_for_resource__, self.log_groups) def __describe_metric_filters__(self, regional_client): logger.info("CloudWatch Logs - Describing metric filters...") @@ -214,21 +214,19 @@ def __get_log_events__(self, regional_client): f"CloudWatch Logs - Finished retrieving log events in {regional_client.region}..." ) - def __list_tags_for_resource__(self): - logger.info("CloudWatch Logs - List Tags...") + def __list_tags_for_resource__(self, log_group): + logger.info(f"CloudWatch Logs - List Tags for Log Group {log_group.name}...") try: - for log_group in self.log_groups: - try: - regional_client = self.regional_clients[log_group.region] - response = regional_client.list_tags_log_group( - logGroupName=log_group.name - )["tags"] - log_group.tags = [response] - except ClientError as error: - if error.response["Error"]["Code"] == "ResourceNotFoundException": - log_group.tags = [] - - continue + regional_client = self.regional_clients[log_group.region] + response = regional_client.list_tags_for_resource( + resourceArn=log_group.arn + )["tags"] + log_group.tags = [response] + except ClientError as error: + if error.response["Error"]["Code"] == "ResourceNotFoundException": + logger.warning( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" diff --git a/prowler/providers/aws/services/codebuild/codebuild_project_older_90_days/codebuild_project_older_90_days.py b/prowler/providers/aws/services/codebuild/codebuild_project_older_90_days/codebuild_project_older_90_days.py index 988ee86e28e..bc730a61bf9 100644 --- a/prowler/providers/aws/services/codebuild/codebuild_project_older_90_days/codebuild_project_older_90_days.py +++ b/prowler/providers/aws/services/codebuild/codebuild_project_older_90_days/codebuild_project_older_90_days.py @@ -7,7 +7,7 @@ class codebuild_project_older_90_days(Check): def execute(self): findings = [] - for project in codebuild_client.projects: + for project in codebuild_client.projects.values(): report = Check_Report_AWS(self.metadata()) report.region = project.region report.resource_id = project.name diff --git a/prowler/providers/aws/services/codebuild/codebuild_project_user_controlled_buildspec/codebuild_project_user_controlled_buildspec.py b/prowler/providers/aws/services/codebuild/codebuild_project_user_controlled_buildspec/codebuild_project_user_controlled_buildspec.py index 31a66622146..d41d76641ec 100644 --- a/prowler/providers/aws/services/codebuild/codebuild_project_user_controlled_buildspec/codebuild_project_user_controlled_buildspec.py +++ b/prowler/providers/aws/services/codebuild/codebuild_project_user_controlled_buildspec/codebuild_project_user_controlled_buildspec.py @@ -7,7 +7,7 @@ class codebuild_project_user_controlled_buildspec(Check): def execute(self): findings = [] - for project in codebuild_client.projects: + for project in codebuild_client.projects.values(): report = Check_Report_AWS(self.metadata()) report.region = project.region report.resource_id = project.name diff --git a/prowler/providers/aws/services/codebuild/codebuild_service.py b/prowler/providers/aws/services/codebuild/codebuild_service.py index 9b0ab4191bb..49613c56fa6 100644 --- a/prowler/providers/aws/services/codebuild/codebuild_service.py +++ b/prowler/providers/aws/services/codebuild/codebuild_service.py @@ -1,7 +1,8 @@ import datetime -from dataclasses import dataclass from typing import Optional +from pydantic import BaseModel + 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 @@ -12,12 +13,16 @@ class Codebuild(AWSService): def __init__(self, audit_info): # Call AWSService's __init__ super().__init__(__class__.__name__, audit_info) - self.projects = [] + self.projects = {} self.__threading_call__(self.__list_projects__) - self.__list_builds_for_project__() + self.__threading_call__( + self.__list_builds_for_project__, self.projects.values() + ) + self.__threading_call__(self.__batch_get_builds__, self.projects.values()) + self.__threading_call__(self.__batch_get_projects__, self.projects.values()) def __list_projects__(self, regional_client): - logger.info("Codebuild - listing projects") + logger.info("Codebuild - Listing projects...") try: list_projects_paginator = regional_client.get_paginator("list_projects") for page in list_projects_paginator.paginate(): @@ -26,14 +31,10 @@ def __list_projects__(self, regional_client): if not self.audit_resources or ( is_resource_filtered(project_arn, self.audit_resources) ): - self.projects.append( - Project( - name=project, - arn=project_arn, - region=regional_client.region, - last_invoked_time=None, - buildspec=None, - ) + self.projects[project_arn] = Project( + name=project, + arn=project_arn, + region=regional_client.region, ) except Exception as error: @@ -41,38 +42,57 @@ def __list_projects__(self, regional_client): f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) - def __list_builds_for_project__(self): - logger.info("Codebuild - listing builds from projects") + def __list_builds_for_project__(self, project): + logger.info("Codebuild - Listing builds...") try: - for project in self.projects: - for region, client in self.regional_clients.items(): - if project.region == region: - ids = client.list_builds_for_project(projectName=project.name) - if "ids" in ids: - if len(ids["ids"]) > 0: - builds = client.batch_get_builds(ids=[ids["ids"][0]]) - if "builds" in builds: - if "endTime" in builds["builds"][0]: - project.last_invoked_time = builds["builds"][0][ - "endTime" - ] + regional_client = self.regional_clients[project.region] + build_ids = regional_client.list_builds_for_project( + projectName=project.name + ).get("ids", []) + if len(build_ids) > 0: + project.last_build = Build(id=build_ids[0]) + except Exception as error: + logger.error( + f"{project.region}: {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) - projects = client.batch_get_projects(names=[project.name])[ - "projects" - ][0]["source"] - if "buildspec" in projects: - project.buildspec = projects["buildspec"] + def __batch_get_builds__(self, project): + logger.info("Codebuild - Getting builds...") + try: + if project.last_build and project.last_build.id: + regional_client = self.regional_clients[project.region] + builds_by_id = regional_client.batch_get_builds( + ids=[project.last_build.id] + ).get("builds", []) + if len(builds_by_id) > 0: + project.last_invoked_time = builds_by_id[0].get("endTime") + except Exception as error: + logger.error( + f"{regional_client.region}: {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + def __batch_get_projects__(self, project): + logger.info("Codebuild - Getting projects...") + try: + regional_client = self.regional_clients[project.region] + project_source = regional_client.batch_get_projects(names=[project.name])[ + "projects" + ][0]["source"] + project.buildspec = project_source.get("buildspec", "") except Exception as error: logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) -@dataclass -class Project: +class Build(BaseModel): + id: str + + +class Project(BaseModel): name: str arn: str region: str + last_build: Optional[Build] last_invoked_time: Optional[datetime.datetime] buildspec: Optional[str] diff --git a/prowler/providers/aws/services/ec2/ec2_ebs_volume_snapshots_exists/ec2_ebs_volume_snapshots_exists.metadata.json b/prowler/providers/aws/services/ec2/ec2_ebs_volume_snapshots_exists/ec2_ebs_volume_snapshots_exists.metadata.json index 26325e1210a..fa8e660aa86 100644 --- a/prowler/providers/aws/services/ec2/ec2_ebs_volume_snapshots_exists/ec2_ebs_volume_snapshots_exists.metadata.json +++ b/prowler/providers/aws/services/ec2/ec2_ebs_volume_snapshots_exists/ec2_ebs_volume_snapshots_exists.metadata.json @@ -6,10 +6,10 @@ "Data Protection" ], "ServiceName": "ec2", - "SubServiceName": "snapshot", + "SubServiceName": "volume", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsEc2Snapshot", + "ResourceType": "AwsEc2Volume", "Description": "Check if EBS snapshots exists.", "Risk": "Ensure that your EBS volumes (available or in-use) have recent snapshots (taken weekly) available for point-in-time recovery for a better, more reliable data backup strategy.", "RelatedUrl": "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSSnapshots.html", diff --git a/prowler/providers/aws/services/ec2/ec2_instance_managed_by_ssm/ec2_instance_managed_by_ssm.py b/prowler/providers/aws/services/ec2/ec2_instance_managed_by_ssm/ec2_instance_managed_by_ssm.py index 69af056edd9..f35578ef6ed 100644 --- a/prowler/providers/aws/services/ec2/ec2_instance_managed_by_ssm/ec2_instance_managed_by_ssm.py +++ b/prowler/providers/aws/services/ec2/ec2_instance_managed_by_ssm/ec2_instance_managed_by_ssm.py @@ -7,21 +7,23 @@ class ec2_instance_managed_by_ssm(Check): def execute(self): findings = [] for instance in ec2_client.instances: - if instance.state != "terminated": - report = Check_Report_AWS(self.metadata()) - report.region = instance.region - report.resource_arn = instance.arn - report.resource_tags = instance.tags - report.status = "PASS" + report = Check_Report_AWS(self.metadata()) + report.region = instance.region + report.resource_arn = instance.arn + report.resource_tags = instance.tags + report.status = "PASS" + report.status_extended = ( + f"EC2 Instance {instance.id} is managed by Systems Manager." + ) + report.resource_id = instance.id + # instances not running should pass the check + if instance.state in ["pending", "terminated", "stopped"]: + report.status_extended = f"EC2 Instance {instance.id} is unmanaged by Systems Manager because it is {instance.state}." + elif not ssm_client.managed_instances.get(instance.id): + report.status = "FAIL" report.status_extended = ( - f"EC2 Instance {instance.id} is managed by Systems Manager." + f"EC2 Instance {instance.id} is not managed by Systems Manager." ) - report.resource_id = instance.id - if not ssm_client.managed_instances.get(instance.id): - report.status = "FAIL" - report.status_extended = ( - f"EC2 Instance {instance.id} is not managed by Systems Manager." - ) - findings.append(report) + findings.append(report) return findings diff --git a/prowler/providers/aws/services/iam/lib/policy.py b/prowler/providers/aws/services/iam/lib/policy.py new file mode 100644 index 00000000000..f9df81219b9 --- /dev/null +++ b/prowler/providers/aws/services/iam/lib/policy.py @@ -0,0 +1,127 @@ +from ipaddress import ip_address, ip_network + +from prowler.lib.logger import logger + + +def is_policy_cross_account(policy: dict, audited_account: str) -> bool: + """ + is_policy_cross_account checks if the policy allows cross-account access. + Args: + policy (dict): The policy to check. + audited_account (str): The account to check if it has access. + Returns: + bool: True if the policy allows cross-account access, False otherwise. + """ + if policy and "Statement" in policy: + if isinstance(policy["Statement"], list): + for statement in policy["Statement"]: + if statement["Effect"] == "Allow" and "AWS" in statement["Principal"]: + if isinstance(statement["Principal"]["AWS"], list): + for aws_account in statement["Principal"]["AWS"]: + if audited_account not in aws_account or "*" == aws_account: + return True + else: + if ( + audited_account not in statement["Principal"]["AWS"] + or "*" == statement["Principal"]["AWS"] + ): + return True + else: + statement = policy["Statement"] + if statement["Effect"] == "Allow" and "AWS" in statement["Principal"]: + if isinstance(statement["Principal"]["AWS"], list): + for aws_account in statement["Principal"]["AWS"]: + if audited_account not in aws_account or "*" == aws_account: + return True + else: + if ( + audited_account not in statement["Principal"]["AWS"] + or "*" == statement["Principal"]["AWS"] + ): + return True + return False + + +def is_policy_public(policy: dict) -> bool: + """ + is_policy_public checks if the policy is publicly accessible. + If the "Principal" element value is set to { "AWS": "*" } and the policy statement is not using any Condition clauses to filter the access, the selected policy is publicly accessible. + Args: + policy (dict): The policy to check. + Returns: + bool: True if the policy is publicly accessible, False otherwise. + """ + if policy and "Statement" in policy: + for statement in policy["Statement"]: + if ( + "Principal" in statement + and ( + "*" == statement["Principal"] + or "arn:aws:iam::*:root" in statement["Principal"] + ) + and "Condition" not in statement + ): + return True + elif "Principal" in statement and "AWS" in statement["Principal"]: + if isinstance(statement["Principal"]["AWS"], str): + principals = [statement["Principal"]["AWS"]] + else: + principals = statement["Principal"]["AWS"] + for principal_arn in principals: + if ( + principal_arn == "*" or principal_arn == "arn:aws:iam::*:root" + ) and "Condition" not in statement: + return True + return False + + +def is_condition_restricting_from_private_ip(condition_statement: dict) -> bool: + """Check if the policy condition is coming from a private IP address. + + Keyword arguments: + condition_statement -- The policy condition to check. For example: + { + "IpAddress": { + "aws:SourceIp": "X.X.X.X" + } + } + """ + try: + CONDITION_OPERATOR = "IpAddress" + CONDITION_KEY = "aws:sourceip" + + is_from_private_ip = False + + if condition_statement.get(CONDITION_OPERATOR, {}): + # We need to transform the condition_statement into lowercase + condition_statement[CONDITION_OPERATOR] = { + k.lower(): v for k, v in condition_statement[CONDITION_OPERATOR].items() + } + + if condition_statement[CONDITION_OPERATOR].get(CONDITION_KEY, ""): + if not isinstance( + condition_statement[CONDITION_OPERATOR][CONDITION_KEY], list + ): + condition_statement[CONDITION_OPERATOR][CONDITION_KEY] = [ + condition_statement[CONDITION_OPERATOR][CONDITION_KEY] + ] + + for ip in condition_statement[CONDITION_OPERATOR][CONDITION_KEY]: + # Select if IP address or IP network searching in the string for '/' + if "/" in ip: + if not ip_network(ip, strict=False).is_private: + break + else: + if not ip_address(ip).is_private: + break + else: + is_from_private_ip = True + + except ValueError: + logger.error(f"Invalid IP: {ip}") + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + return is_from_private_ip diff --git a/prowler/providers/aws/services/rds/rds_service.py b/prowler/providers/aws/services/rds/rds_service.py index b1e724801bd..608515af586 100644 --- a/prowler/providers/aws/services/rds/rds_service.py +++ b/prowler/providers/aws/services/rds/rds_service.py @@ -46,7 +46,7 @@ def __describe_db_instances__(self, regional_client): DBInstance( id=instance["DBInstanceIdentifier"], arn=arn, - endpoint=instance.get("Endpoint"), + endpoint=instance.get("Endpoint", {}), engine=instance["Engine"], engine_version=instance["EngineVersion"], status=instance["DBInstanceStatus"], @@ -178,7 +178,7 @@ def __describe_db_clusters__(self, regional_client): db_cluster = DBCluster( id=cluster["DBClusterIdentifier"], arn=db_cluster_arn, - endpoint=cluster.get("Endpoint"), + endpoint=cluster.get("Endpoint", ""), engine=cluster["Engine"], status=cluster["Status"], public=cluster.get("PubliclyAccessible", False), @@ -356,7 +356,7 @@ class DBInstance(BaseModel): id: str # arn:{partition}:rds:{region}:{account}:db:{resource_id} arn: str - endpoint: Optional[dict] + endpoint: dict engine: str engine_version: str status: str @@ -381,7 +381,7 @@ class DBInstance(BaseModel): class DBCluster(BaseModel): id: str arn: str - endpoint: Optional[str] + endpoint: str engine: str status: str public: bool diff --git a/prowler/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access.py b/prowler/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access.py index 0042d2198d0..859659e898a 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access.py +++ b/prowler/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access.py @@ -1,4 +1,10 @@ from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.lib.policy_condition_parser.policy_condition_parser import ( + is_condition_block_restrictive, +) +from prowler.providers.aws.services.iam.lib.policy import ( + is_condition_restricting_from_private_ip, +) from prowler.providers.aws.services.s3.s3_client import s3_client from prowler.providers.aws.services.s3.s3control_client import s3control_client @@ -46,26 +52,35 @@ def execute(self): # 4. Check bucket policy if bucket.policy: - for statement in bucket.policy["Statement"]: + for statement in bucket.policy.get("Statement", []): if ( "Principal" in statement - and "*" == statement["Principal"] and statement["Effect"] == "Allow" + and not is_condition_block_restrictive( + statement.get("Condition", {}), "", True + ) + and ( + not is_condition_restricting_from_private_ip( + statement.get("Condition", {}) + ) + if statement.get("Condition", {}).get( + "IpAddress", {} + ) + else True + ) ): - report.status = "FAIL" - report.status_extended = f"S3 Bucket {bucket.name} has public access due to bucket policy." - else: - if ( - "Principal" in statement - and "AWS" in statement["Principal"] - and statement["Effect"] == "Allow" - ): - if isinstance( - statement["Principal"]["AWS"], str - ): - principals = [statement["Principal"]["AWS"]] - else: - principals = statement["Principal"]["AWS"] + if "*" == statement["Principal"]: + report.status = "FAIL" + report.status_extended = f"S3 Bucket {bucket.name} has public access due to bucket policy." + elif "AWS" in statement["Principal"]: + principals = ( + statement["Principal"]["AWS"] + if isinstance( + statement["Principal"]["AWS"], list + ) + else [statement["Principal"]["AWS"]] + ) + for principal_arn in principals: if principal_arn == "*": report.status = "FAIL" diff --git a/prowler/providers/aws/services/s3/s3_bucket_secure_transport_policy/s3_bucket_secure_transport_policy.py b/prowler/providers/aws/services/s3/s3_bucket_secure_transport_policy/s3_bucket_secure_transport_policy.py index ab529b55998..0a8a193d3bf 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_secure_transport_policy/s3_bucket_secure_transport_policy.py +++ b/prowler/providers/aws/services/s3/s3_bucket_secure_transport_policy/s3_bucket_secure_transport_policy.py @@ -22,6 +22,7 @@ def execute(self): if ( statement["Effect"] == "Deny" and "Condition" in statement + and "Action" in statement and ( "s3:PutObject" in statement["Action"] or "*" in statement["Action"] diff --git a/prowler/providers/aws/services/sns/sns_topics_kms_encryption_at_rest_enabled/sns_topics_kms_encryption_at_rest_enabled.metadata.json b/prowler/providers/aws/services/sns/sns_topics_kms_encryption_at_rest_enabled/sns_topics_kms_encryption_at_rest_enabled.metadata.json index ad1e000a93e..0826f89219f 100644 --- a/prowler/providers/aws/services/sns/sns_topics_kms_encryption_at_rest_enabled/sns_topics_kms_encryption_at_rest_enabled.metadata.json +++ b/prowler/providers/aws/services/sns/sns_topics_kms_encryption_at_rest_enabled/sns_topics_kms_encryption_at_rest_enabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:aws:sns:region:account-id:topic", "Severity": "high", - "ResourceType": "AwsSNSTopic", + "ResourceType": "AwsSnsTopic", "Description": "Ensure there are no SNS Topics unencrypted", "Risk": "If not enabled sensitive information at rest is not protected.", "RelatedUrl": "https://docs.aws.amazon.com/sns/latest/dg/sns-server-side-encryption.html", diff --git a/prowler/providers/aws/services/sns/sns_topics_not_publicly_accessible/sns_topics_not_publicly_accessible.metadata.json b/prowler/providers/aws/services/sns/sns_topics_not_publicly_accessible/sns_topics_not_publicly_accessible.metadata.json index 7572ab4f4ca..d6d7ad3a3f4 100644 --- a/prowler/providers/aws/services/sns/sns_topics_not_publicly_accessible/sns_topics_not_publicly_accessible.metadata.json +++ b/prowler/providers/aws/services/sns/sns_topics_not_publicly_accessible/sns_topics_not_publicly_accessible.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:aws:sns:region:account-id:topic", "Severity": "high", - "ResourceType": "AwsSNSTopic", + "ResourceType": "AwsSnsTopic", "Description": "Check if SNS topics have policy set as Public", "Risk": "Publicly accessible services could expose sensitive data to bad actors.", "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/sns-topic-policy.html", diff --git a/prowler/providers/aws/services/ssm/ssm_service.py b/prowler/providers/aws/services/ssm/ssm_service.py index 8aaea837f5d..c0f504dc1a3 100644 --- a/prowler/providers/aws/services/ssm/ssm_service.py +++ b/prowler/providers/aws/services/ssm/ssm_service.py @@ -1,4 +1,5 @@ import json +import time from enum import Enum from typing import Optional @@ -145,6 +146,10 @@ def __describe_instance_information__(self, regional_client): id=resource_id, region=regional_client.region, ) + # boto3 does not properly handle throttling exceptions for + # ssm:DescribeInstanceInformation when there are large numbers of instances + # AWS support recommends manually reducing frequency of requests + time.sleep(0.1) except Exception as error: logger.error( diff --git a/prowler/providers/azure/services/network/network_service.py b/prowler/providers/azure/services/network/network_service.py index 1a7d55c8b33..f0dcadcdc8f 100644 --- a/prowler/providers/azure/services/network/network_service.py +++ b/prowler/providers/azure/services/network/network_service.py @@ -6,7 +6,6 @@ from prowler.providers.azure.lib.service.service import AzureService -########################## SQLServer class Network(AzureService): def __init__(self, audit_info): super().__init__(NetworkManagementClient, audit_info) diff --git a/prowler/providers/azure/services/network/network_watcher_enabled/network_watcher_enabled.metadata.json b/prowler/providers/azure/services/network/network_watcher_enabled/network_watcher_enabled.metadata.json index 83513a2073f..f80c3d7f1f7 100644 --- a/prowler/providers/azure/services/network/network_watcher_enabled/network_watcher_enabled.metadata.json +++ b/prowler/providers/azure/services/network/network_watcher_enabled/network_watcher_enabled.metadata.json @@ -1,7 +1,7 @@ { "Provider": "azure", "CheckID": "network_watcher_enabled", - "CheckTitle": "Ensure that Network Watcher is 'Enabled'", + "CheckTitle": "Ensure that Network Watcher is 'Enabled' for all locations in the Azure subscription", "CheckType": [], "ServiceName": "network", "SubServiceName": "", @@ -15,12 +15,12 @@ "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/Network/enable-network-watcher.html#", + "Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/Network/enable-network-watcher.html", "Terraform": "" }, "Recommendation": { "Text": "Opting out of Network Watcher automatic enablement is a permanent change. Once you opt-out you cannot opt-in without contacting support.", - "Url": "https://docs.azure.cn/zh-cn/cli/network/watcher?view=azure-cli-latest#az_network_watcher_list" + "Url": "https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v2-logging-threat-detection#lt-3-enable-logging-for-azure-network-activities" } }, "Categories": [], diff --git a/prowler/providers/azure/services/network/network_watcher_enabled/network_watcher_enabled.py b/prowler/providers/azure/services/network/network_watcher_enabled/network_watcher_enabled.py index e4d6797ed91..02694561f06 100644 --- a/prowler/providers/azure/services/network/network_watcher_enabled/network_watcher_enabled.py +++ b/prowler/providers/azure/services/network/network_watcher_enabled/network_watcher_enabled.py @@ -3,25 +3,26 @@ class network_watcher_enabled(Check): - def execute(self) -> Check_Report_Azure: + def execute(self) -> list[Check_Report_Azure]: findings = [] - nw_locations = [] for subscription, network_watchers in network_client.network_watchers.items(): - for network_watcher in network_watchers: - nw_locations.append(network_watcher.location) - for subscription, locations in network_client.locations.items(): - for location in locations: - report = Check_Report_Azure(self.metadata()) - report.subscription = subscription - report.resource_name = "Network Watcher" - report.resource_id = f"/subscriptions/{subscription}/providers/Microsoft.Network/networkWatchers/{location}" - if location not in nw_locations: - report.status = "FAIL" - report.status_extended = f"Network Watcher is not enabled for the location {location} in subscription {subscription}." - findings.append(report) - else: - report.status = "PASS" - report.status_extended = f"Network Watcher is enabled for the location {location} in subscription {subscription}." - findings.append(report) + report = Check_Report_Azure(self.metadata()) + report.subscription = subscription + report.resource_name = "Network Watcher" + 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( + network_watcher.location for network_watcher in network_watchers + ) + + if missing_locations: + report.status = "FAIL" + report.status_extended = f"Network Watcher is not enabled for the following locations in subscription '{subscription}': {', '.join(missing_locations)}." + else: + report.status = "PASS" + report.status_extended = f"Network Watcher is enabled for all locations in subscription '{subscription}'." + + findings.append(report) return findings diff --git a/tests/config/config_test.py b/tests/config/config_test.py index 0f64d7eb524..57c5350f914 100644 --- a/tests/config/config_test.py +++ b/tests/config/config_test.py @@ -24,6 +24,44 @@ def mock_prowler_get_latest_release(_, **kwargs): return response +old_config_aws = { + "shodan_api_key": None, + "max_security_group_rules": 50, + "max_ec2_instance_age_in_days": 180, + "trusted_account_ids": [], + "log_group_retention_days": 365, + "max_idle_disconnect_timeout_in_seconds": 600, + "max_disconnect_timeout_in_seconds": 300, + "max_session_duration_seconds": 36000, + "obsolete_lambda_runtimes": [ + "java8", + "go1.x", + "provided", + "python3.6", + "python2.7", + "python3.7", + "nodejs4.3", + "nodejs4.3-edge", + "nodejs6.10", + "nodejs", + "nodejs8.10", + "nodejs10.x", + "nodejs12.x", + "nodejs14.x", + "dotnet5.0", + "dotnetcore1.0", + "dotnetcore2.0", + "dotnetcore2.1", + "dotnetcore3.1", + "ruby2.5", + "ruby2.7", + ], + "organizations_enabled_regions": [], + "organizations_trusted_delegated_administrators": [], + "check_rds_instance_replicas": False, + "days_to_expire_threshold": 7, +} + config_aws = { "shodan_api_key": None, "max_security_group_rules": 50, @@ -59,9 +97,17 @@ def mock_prowler_get_latest_release(_, **kwargs): "organizations_enabled_regions": [], "organizations_trusted_delegated_administrators": [], "check_rds_instance_replicas": False, + "days_to_expire_threshold": 7, } -config_azure = {"shodan_api_key": None} +config_azure = { + "shodan_api_key": None, + "php_latest_version": "8.2", + "python_latest_version": "3.12", + "java_latest_version": "17", +} + +config_gcp = {"shodan_api_key": None} class Test_Config: @@ -181,7 +227,6 @@ def test_load_and_validate_config_file_aws(self): path = pathlib.Path(os.path.dirname(os.path.realpath(__file__))) config_test_file = f"{path}/fixtures/config.yaml" provider = "aws" - assert load_and_validate_config_file(provider, config_test_file) == config_aws def test_load_and_validate_config_file_gcp(self): @@ -189,7 +234,7 @@ def test_load_and_validate_config_file_gcp(self): config_test_file = f"{path}/fixtures/config.yaml" provider = "gcp" - assert load_and_validate_config_file(provider, config_test_file) is None + assert load_and_validate_config_file(provider, config_test_file) == config_gcp def test_load_and_validate_config_file_azure(self): path = pathlib.Path(os.path.dirname(os.path.realpath(__file__))) @@ -201,7 +246,6 @@ def test_load_and_validate_config_file_azure(self): def test_load_and_validate_config_file_old_format(self): path = pathlib.Path(os.path.dirname(os.path.realpath(__file__))) config_test_file = f"{path}/fixtures/config_old.yaml" - - assert load_and_validate_config_file("aws", config_test_file) == config_aws + assert load_and_validate_config_file("aws", config_test_file) == old_config_aws assert load_and_validate_config_file("gcp", config_test_file) == {} assert load_and_validate_config_file("azure", config_test_file) == {} diff --git a/tests/config/fixtures/config.yaml b/tests/config/fixtures/config.yaml index c4992e822b8..4f155057a63 100644 --- a/tests/config/fixtures/config.yaml +++ b/tests/config/fixtures/config.yaml @@ -1,7 +1,10 @@ # AWS Configuration aws: + # AWS Global Configuration + # AWS EC2 Configuration # aws.ec2_elastic_ip_shodan + # TODO: create common config shodan_api_key: null # aws.ec2_securitygroup_with_many_ingress_egress_rules --> by default is 50 rules max_security_group_rules: 50 @@ -54,24 +57,41 @@ aws: ] # AWS Organizations - # organizations_scp_check_deny_regions - # organizations_enabled_regions: [ - # 'eu-central-1', - # 'eu-west-1', + # aws.organizations_scp_check_deny_regions + # aws.organizations_enabled_regions: [ + # "eu-central-1", + # "eu-west-1", # "us-east-1" # ] organizations_enabled_regions: [] organizations_trusted_delegated_administrators: [] + # AWS RDS Configuration # aws.rds_instance_backup_enabled # Whether to check RDS instance replicas or not check_rds_instance_replicas: False + # AWS ACM Configuration + # aws.acm_certificates_expiration_check + days_to_expire_threshold: 7 + # Azure Configuration azure: # Azure Network Configuration # azure.network_public_ip_shodan + # TODO: create common config shodan_api_key: null + # Azure App Service + # azure.app_ensure_php_version_is_latest + php_latest_version: "8.2" + # azure.app_ensure_python_version_is_latest + python_latest_version: "3.12" + # azure.app_ensure_java_version_is_latest + java_latest_version: "17" + # GCP Configuration gcp: + # GCP Compute Configuration + # gcp.compute_public_address_shodan + shodan_api_key: null diff --git a/tests/config/fixtures/config_old.yaml b/tests/config/fixtures/config_old.yaml index 11f17af538f..348aef2dcde 100644 --- a/tests/config/fixtures/config_old.yaml +++ b/tests/config/fixtures/config_old.yaml @@ -64,3 +64,7 @@ organizations_trusted_delegated_administrators: [] # aws.rds_instance_backup_enabled # Whether to check RDS instance replicas or not check_rds_instance_replicas: False + +# AWS ACM Configuration +# aws.acm_certificates_expiration_check +days_to_expire_threshold: 7 diff --git a/tests/lib/check/compliance_check_test.py b/tests/lib/check/compliance_check_test.py new file mode 100644 index 00000000000..634a11de6bc --- /dev/null +++ b/tests/lib/check/compliance_check_test.py @@ -0,0 +1,193 @@ +from prowler.lib.check.compliance import update_checks_metadata_with_compliance +from prowler.lib.check.compliance_models import ( + CIS_Requirement_Attribute, + CIS_Requirement_Attribute_AssessmentStatus, + CIS_Requirement_Attribute_Profile, + Compliance_Base_Model, + Compliance_Requirement, +) +from prowler.lib.check.models import Check_Metadata_Model + + +class TestCompliance: + provider = "aws" + + def get_custom_framework(self): + return { + "framework1": Compliance_Base_Model( + Framework="Framework1", + Provider="Provider1", + Version="1.0", + Description="Framework 1 Description", + Requirements=[ + Compliance_Requirement( + Id="1.1.1", + Description="description", + Attributes=[ + CIS_Requirement_Attribute( + Section="1. Identity", + Profile=CIS_Requirement_Attribute_Profile("Level 1"), + AssessmentStatus=CIS_Requirement_Attribute_AssessmentStatus( + "Manual" + ), + Description="Description", + RationaleStatement="Rationale", + ImpactStatement="Impact", + RemediationProcedure="Remediation", + AuditProcedure="Audit", + AdditionalInformation="Additional", + References="References", + ) + ], + Checks=["check1", "check2"], + ), + # Manual requirement + Compliance_Requirement( + Id="1.1.2", + Description="description", + Attributes=[ + CIS_Requirement_Attribute( + Section="1. Identity", + Profile=CIS_Requirement_Attribute_Profile("Level 1"), + AssessmentStatus=CIS_Requirement_Attribute_AssessmentStatus( + "Manual" + ), + Description="Description", + RationaleStatement="Rationale", + ImpactStatement="Impact", + RemediationProcedure="Remediation", + AuditProcedure="Audit", + AdditionalInformation="Additional", + References="References", + ) + ], + Checks=[], + ), + ], + ) + } + + def get_custom_check_metadata(self): + return { + "check1": Check_Metadata_Model( + Provider="aws", + CheckID="check1", + CheckTitle="Check 1", + CheckType=["type1"], + ServiceName="service1", + SubServiceName="subservice1", + ResourceIdTemplate="template1", + Severity="high", + ResourceType="resource1", + Description="Description 1", + Risk="risk1", + RelatedUrl="url1", + Remediation={ + "Code": { + "CLI": "cli1", + "NativeIaC": "native1", + "Other": "other1", + "Terraform": "terraform1", + }, + "Recommendation": {"Text": "text1", "Url": "url1"}, + }, + Categories=["categoryone"], + DependsOn=["dependency1"], + RelatedTo=["related1"], + Notes="notes1", + Compliance=[], + ), + "check2": Check_Metadata_Model( + Provider="aws", + CheckID="check2", + CheckTitle="Check 2", + CheckType=["type2"], + ServiceName="service2", + SubServiceName="subservice2", + ResourceIdTemplate="template2", + Severity="medium", + ResourceType="resource2", + Description="Description 2", + Risk="risk2", + RelatedUrl="url2", + Remediation={ + "Code": { + "CLI": "cli2", + "NativeIaC": "native2", + "Other": "other2", + "Terraform": "terraform2", + }, + "Recommendation": {"Text": "text2", "Url": "url2"}, + }, + Categories=["categorytwo"], + DependsOn=["dependency2"], + RelatedTo=["related2"], + Notes="notes2", + Compliance=[], + ), + } + + def test_update_checks_metadata(self): + bulk_compliance_frameworks = self.get_custom_framework() + bulk_checks_metadata = self.get_custom_check_metadata() + + updated_metadata = update_checks_metadata_with_compliance( + bulk_compliance_frameworks, bulk_checks_metadata + ) + + assert "check1" in updated_metadata + assert "check2" in updated_metadata + assert "manual_check" in updated_metadata + + manual_compliance = updated_metadata["manual_check"].Compliance[0] + check1_compliance = updated_metadata["check1"].Compliance[0] + + assert len(updated_metadata["manual_check"].Compliance) == 1 + assert len(updated_metadata["check1"].Compliance) == 1 + + assert manual_compliance.Framework == "Framework1" + assert manual_compliance.Provider == "Provider1" + assert manual_compliance.Version == "1.0" + assert manual_compliance.Description == "Framework 1 Description" + assert len(manual_compliance.Requirements) == 1 + + manual_requirement = manual_compliance.Requirements[0] + assert manual_requirement.Id == "1.1.2" + assert manual_requirement.Description == "description" + assert len(manual_requirement.Attributes) == 1 + + manual_attribute = manual_requirement.Attributes[0] + assert manual_attribute.Section == "1. Identity" + assert manual_attribute.Profile == "Level 1" + assert manual_attribute.AssessmentStatus == "Manual" + assert manual_attribute.Description == "Description" + assert manual_attribute.RationaleStatement == "Rationale" + assert manual_attribute.ImpactStatement == "Impact" + assert manual_attribute.RemediationProcedure == "Remediation" + assert manual_attribute.AuditProcedure == "Audit" + assert manual_attribute.AdditionalInformation == "Additional" + assert manual_attribute.References == "References" + + assert len(updated_metadata["check1"].Compliance) == 1 + assert check1_compliance.Framework == "Framework1" + assert check1_compliance.Provider == "Provider1" + assert check1_compliance.Version == "1.0" + assert check1_compliance.Description == "Framework 1 Description" + assert len(check1_compliance.Requirements) == 1 + + check1_requirement = check1_compliance.Requirements[0] + assert check1_requirement.Id == "1.1.1" + assert check1_requirement.Description == "description" + assert len(check1_requirement.Attributes) == 1 + + check1_attribute = check1_requirement.Attributes[0] + assert check1_attribute.Section == "1. Identity" + assert check1_attribute.Profile == "Level 1" + assert check1_attribute.AssessmentStatus == "Manual" + assert check1_attribute.Description == "Description" + assert check1_attribute.RationaleStatement == "Rationale" + assert check1_attribute.ImpactStatement == "Impact" + assert check1_attribute.RemediationProcedure == "Remediation" + assert check1_attribute.AuditProcedure == "Audit" + assert check1_attribute.AdditionalInformation == "Additional" + assert check1_attribute.References == "References" diff --git a/tests/providers/aws/lib/policy_condition_parser/policy_condition_parser_test.py b/tests/providers/aws/lib/policy_condition_parser/policy_condition_parser_test.py index c7b000b5d69..120b8be4e97 100644 --- a/tests/providers/aws/lib/policy_condition_parser/policy_condition_parser_test.py +++ b/tests/providers/aws/lib/policy_condition_parser/policy_condition_parser_test.py @@ -1366,3 +1366,26 @@ def test_condition_parser_allowing_cross_account_with_invalid_block(self): assert not is_condition_block_restrictive( condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER, True ) + + def test_condition_parser_string_equals_vpc(self): + condition_statement = {"StringEquals": {"aws:SourceVpc": "vpc-123456"}} + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER, True + ) + + def test_condition_parser_string_equals_vpc_list(self): + condition_statement = {"StringEquals": {"aws:sourcevpc": ["vpc-123456"]}} + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER, True + ) + + def test_condition_parser_string_equals_vpc_list_not_valid(self): + condition_statement = { + "StringEquals": {"aws:SourceVpc": ["vpc-123456", "vpc-654321"]} + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER, True + ) diff --git a/tests/providers/aws/services/acm/acm_certificates_expiration_check/acm_certificates_expiration_check_test.py b/tests/providers/aws/services/acm/acm_certificates_expiration_check/acm_certificates_expiration_check_test.py index 010e84f1121..a0495b55640 100644 --- a/tests/providers/aws/services/acm/acm_certificates_expiration_check/acm_certificates_expiration_check_test.py +++ b/tests/providers/aws/services/acm/acm_certificates_expiration_check/acm_certificates_expiration_check_test.py @@ -33,6 +33,7 @@ def test_acm_certificate_expirated(self): certificate_name = "test-certificate.com" certificate_type = "AMAZON_ISSUED" expiration_days = 5 + in_use = True acm_client = mock.MagicMock acm_client.certificates = [ @@ -42,11 +43,14 @@ def test_acm_certificate_expirated(self): name=certificate_name, type=certificate_type, expiration_days=expiration_days, + in_use=in_use, transparency_logging=True, region=AWS_REGION, ) ] + acm_client.audit_config = {"days_to_expire_threshold": 7} + with mock.patch( "prowler.providers.aws.services.acm.acm_service.ACM", new=acm_client, @@ -76,6 +80,7 @@ def test_acm_certificate_expirated_long_time(self): certificate_name = "test-certificate.com" certificate_type = "AMAZON_ISSUED" expiration_days = -400 + in_use = True acm_client = mock.MagicMock acm_client.certificates = [ @@ -85,16 +90,18 @@ def test_acm_certificate_expirated_long_time(self): name=certificate_name, type=certificate_type, expiration_days=expiration_days, + in_use=in_use, transparency_logging=True, region=AWS_REGION, ) ] + acm_client.audit_config = {"days_to_expire_threshold": 7} + with mock.patch( "prowler.providers.aws.services.acm.acm_service.ACM", new=acm_client, ): - # Test Check from prowler.providers.aws.services.acm.acm_certificates_expiration_check.acm_certificates_expiration_check import ( acm_certificates_expiration_check, ) @@ -119,6 +126,7 @@ def test_acm_certificate_not_expirated(self): certificate_name = "test-certificate.com" certificate_type = "AMAZON_ISSUED" expiration_days = 365 + in_use = True acm_client = mock.MagicMock acm_client.certificates = [ @@ -128,16 +136,18 @@ def test_acm_certificate_not_expirated(self): name=certificate_name, type=certificate_type, expiration_days=expiration_days, + in_use=in_use, transparency_logging=True, region=AWS_REGION, ) ] + acm_client.audit_config = {"days_to_expire_threshold": 7} + with mock.patch( "prowler.providers.aws.services.acm.acm_service.ACM", new=acm_client, ): - # Test Check from prowler.providers.aws.services.acm.acm_certificates_expiration_check.acm_certificates_expiration_check import ( acm_certificates_expiration_check, ) @@ -155,3 +165,90 @@ def test_acm_certificate_not_expirated(self): assert result[0].resource_arn == certificate_arn assert result[0].region == AWS_REGION assert result[0].resource_tags == [] + + def test_acm_certificate_not_in_use(self): + certificate_id = str(uuid.uuid4()) + certificate_arn = f"arn:aws:acm:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:certificate/{certificate_id}" + certificate_name = "test-certificate.com" + certificate_type = "AMAZON_ISSUED" + expiration_days = 365 + in_use = False + + acm_client = mock.MagicMock + acm_client.certificates = [ + Certificate( + arn=certificate_arn, + id=certificate_id, + name=certificate_name, + type=certificate_type, + expiration_days=expiration_days, + in_use=in_use, + transparency_logging=True, + region=AWS_REGION, + ) + ] + + acm_client.audit_config = {"days_to_expire_threshold": 7} + + acm_client.provider = mock.MagicMock(scan_unused_services=False) + + with mock.patch( + "prowler.providers.aws.services.acm.acm_service.ACM", + new=acm_client, + ): + from prowler.providers.aws.services.acm.acm_certificates_expiration_check.acm_certificates_expiration_check import ( + acm_certificates_expiration_check, + ) + + check = acm_certificates_expiration_check() + result = check.execute() + + assert len(result) == 0 + + def test_acm_certificate_not_in_use_expired_scan_unused_services(self): + certificate_id = str(uuid.uuid4()) + certificate_arn = f"arn:aws:acm:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:certificate/{certificate_id}" + certificate_name = "test-certificate.com" + certificate_type = "AMAZON_ISSUED" + expiration_days = -400 + in_use = False + + acm_client = mock.MagicMock + acm_client.certificates = [ + Certificate( + arn=certificate_arn, + id=certificate_id, + name=certificate_name, + type=certificate_type, + expiration_days=expiration_days, + in_use=in_use, + transparency_logging=True, + region=AWS_REGION, + ) + ] + + acm_client.audit_config = {"days_to_expire_threshold": 7} + + acm_client.provider = mock.MagicMock(scan_unused_services=True) + + with mock.patch( + "prowler.providers.aws.services.acm.acm_service.ACM", + new=acm_client, + ): + from prowler.providers.aws.services.acm.acm_certificates_expiration_check.acm_certificates_expiration_check import ( + acm_certificates_expiration_check, + ) + + check = acm_certificates_expiration_check() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"ACM Certificate {certificate_id} for {certificate_name} has expired ({abs(expiration_days)} days ago)." + ) + assert result[0].resource_id == certificate_id + assert result[0].resource_arn == certificate_arn + assert result[0].region == AWS_REGION + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/acm/acm_certificates_transparency_logs_enabled/acm_certificates_transparency_logs_enabled_test.py b/tests/providers/aws/services/acm/acm_certificates_transparency_logs_enabled/acm_certificates_transparency_logs_enabled_test.py index 11ff3581f8b..3868b0aec20 100644 --- a/tests/providers/aws/services/acm/acm_certificates_transparency_logs_enabled/acm_certificates_transparency_logs_enabled_test.py +++ b/tests/providers/aws/services/acm/acm_certificates_transparency_logs_enabled/acm_certificates_transparency_logs_enabled_test.py @@ -41,6 +41,7 @@ def test_acm_certificate_with_logging(self): type=certificate_type, expiration_days=365, transparency_logging=True, + in_use=True, region=AWS_REGION, ) ] @@ -83,6 +84,7 @@ def test_acm_certificate_without_logging(self): type=certificate_type, expiration_days=365, transparency_logging=False, + in_use=True, region=AWS_REGION, ) ] diff --git a/tests/providers/aws/services/codebuild/codebuild_project_older_90_days/codebuild_project_older_90_days_test.py b/tests/providers/aws/services/codebuild/codebuild_project_older_90_days/codebuild_project_older_90_days_test.py index fd13f0eae06..236c0820ae2 100644 --- a/tests/providers/aws/services/codebuild/codebuild_project_older_90_days/codebuild_project_older_90_days_test.py +++ b/tests/providers/aws/services/codebuild/codebuild_project_older_90_days/codebuild_project_older_90_days_test.py @@ -13,15 +13,16 @@ def test_project_not_built_in_last_90_days(self): codebuild_client = mock.MagicMock project_name = "test-project" project_arn = f"arn:aws:codebuild:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:project/{project_name}" - codebuild_client.projects = [ - Project( + codebuild_client.projects = { + project_arn: Project( name=project_name, arn=project_arn, region="eu-west-1", last_invoked_time=datetime.now(timezone.utc) - timedelta(days=100), buildspec=None, ) - ] + } + with mock.patch( "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", codebuild_client, @@ -47,15 +48,16 @@ def test_project_not_built(self): codebuild_client = mock.MagicMock project_name = "test-project" project_arn = f"arn:aws:codebuild:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:project/{project_name}" - codebuild_client.projects = [ - Project( + codebuild_client.projects = { + project_arn: Project( name=project_name, arn=project_arn, region="eu-west-1", last_invoked_time=None, buildspec=None, ) - ] + } + with mock.patch( "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", codebuild_client, @@ -79,15 +81,16 @@ def test_project_built_in_last_90_days(self): codebuild_client = mock.MagicMock project_name = "test-project" project_arn = f"arn:aws:codebuild:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:project/{project_name}" - codebuild_client.projects = [ - Project( + codebuild_client.projects = { + project_arn: Project( name=project_name, arn=project_arn, region="eu-west-1", last_invoked_time=datetime.now(timezone.utc) - timedelta(days=10), buildspec=None, ) - ] + } + with mock.patch( "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", codebuild_client, diff --git a/tests/providers/aws/services/codebuild/codebuild_project_user_controlled_buildspec/codebuild_project_user_controlled_buildspec_test.py b/tests/providers/aws/services/codebuild/codebuild_project_user_controlled_buildspec/codebuild_project_user_controlled_buildspec_test.py index d9a30a7e0a7..bd213e29bd0 100644 --- a/tests/providers/aws/services/codebuild/codebuild_project_user_controlled_buildspec/codebuild_project_user_controlled_buildspec_test.py +++ b/tests/providers/aws/services/codebuild/codebuild_project_user_controlled_buildspec/codebuild_project_user_controlled_buildspec_test.py @@ -12,15 +12,15 @@ def test_project_not_buildspec(self): codebuild_client = mock.MagicMock project_name = "test-project" project_arn = f"arn:aws:codebuild:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:project/{project_name}" - codebuild_client.projects = [ - Project( + codebuild_client.projects = { + project_arn: Project( name=project_name, arn=project_arn, region="eu-west-1", last_invoked_time=None, buildspec=None, ) - ] + } with mock.patch( "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", codebuild_client, @@ -47,15 +47,16 @@ def test_project_buildspec_not_yaml(self): codebuild_client = mock.MagicMock project_name = "test-project" project_arn = f"arn:aws:codebuild:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:project/{project_name}" - codebuild_client.projects = [ - Project( + codebuild_client.projects = { + project_arn: Project( name=project_name, arn=project_arn, region="eu-west-1", last_invoked_time=None, buildspec="arn:aws:s3:::my-codebuild-sample2/buildspec.out", ) - ] + } + with mock.patch( "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", codebuild_client, @@ -82,15 +83,15 @@ def test_project_valid_buildspec(self): codebuild_client = mock.MagicMock project_name = "test-project" project_arn = f"arn:aws:codebuild:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:project/{project_name}" - codebuild_client.projects = [ - Project( + codebuild_client.projects = { + project_arn: Project( name=project_name, arn=project_arn, region="eu-west-1", last_invoked_time=None, buildspec="arn:aws:s3:::my-codebuild-sample2/buildspec.yaml", ) - ] + } with mock.patch( "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", codebuild_client, @@ -116,15 +117,15 @@ def test_project_invalid_buildspec_without_extension(self): codebuild_client = mock.MagicMock project_name = "test-project" project_arn = f"arn:aws:codebuild:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:project/{project_name}" - codebuild_client.projects = [ - Project( + codebuild_client.projects = { + project_arn: Project( name=project_name, arn=project_arn, region="eu-west-1", last_invoked_time=None, buildspec="arn:aws:s3:::my-codebuild-sample2/buildspecyaml", ) - ] + } with mock.patch( "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", codebuild_client, diff --git a/tests/providers/aws/services/codebuild/codebuild_service_test.py b/tests/providers/aws/services/codebuild/codebuild_service_test.py index a3c600ff422..77ac6571e57 100644 --- a/tests/providers/aws/services/codebuild/codebuild_service_test.py +++ b/tests/providers/aws/services/codebuild/codebuild_service_test.py @@ -3,25 +3,34 @@ import botocore -from prowler.providers.aws.services.codebuild.codebuild_service import Codebuild +from prowler.providers.aws.services.codebuild.codebuild_service import ( + Build, + Codebuild, + Project, +) from tests.providers.aws.audit_info_utils import ( + AWS_ACCOUNT_NUMBER, + AWS_COMMERCIAL_PARTITION, AWS_REGION_EU_WEST_1, set_mocked_aws_audit_info, ) -# last time invoked time +project_name = "test" +project_arn = f"arn:{AWS_COMMERCIAL_PARTITION}:codebuild:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:project/{project_name}" +build_spec_project_arn = "arn:aws:s3:::my-codebuild-sample2/buildspec.yml" +buildspec_type = "S3" +build_id = "test:93f838a7-cd20-48ae-90e5-c10fbbc78ca6" last_invoked_time = datetime.now() - timedelta(days=2) - # Mocking batch_get_projects make_api_call = botocore.client.BaseClient._make_api_call def mock_make_api_call(self, operation_name, kwarg): if operation_name == "ListProjects": - return {"projects": ["test"]} + return {"projects": [project_name]} if operation_name == "ListBuildsForProject": - return {"ids": ["test:93f838a7-cd20-48ae-90e5-c10fbbc78ca6"]} + return {"ids": [build_id]} if operation_name == "BatchGetBuilds": return {"builds": [{"endTime": last_invoked_time}]} if operation_name == "BatchGetProjects": @@ -29,7 +38,8 @@ def mock_make_api_call(self, operation_name, kwarg): "projects": [ { "source": { - "buildspec": "arn:aws:s3:::my-codebuild-sample2/buildspec.yml" + "type": buildspec_type, + "buildspec": build_spec_project_arn, } } ] @@ -65,16 +75,33 @@ def test__get_service__(self): def test__list_projects__(self): codebuild = Codebuild(set_mocked_aws_audit_info()) assert len(codebuild.projects) == 1 - assert codebuild.projects[0].name == "test" - assert codebuild.projects[0].region == AWS_REGION_EU_WEST_1 + print(codebuild.projects) + assert codebuild.projects[project_arn].name == "test" + assert codebuild.projects[project_arn].region == AWS_REGION_EU_WEST_1 def test__list_builds_for_project__(self): codebuild = Codebuild(set_mocked_aws_audit_info()) assert len(codebuild.projects) == 1 - assert codebuild.projects[0].name == "test" - assert codebuild.projects[0].region == AWS_REGION_EU_WEST_1 - assert codebuild.projects[0].last_invoked_time == last_invoked_time + assert codebuild.projects[project_arn].name == "test" + assert codebuild.projects[project_arn].region == AWS_REGION_EU_WEST_1 + assert codebuild.projects[project_arn].last_invoked_time == last_invoked_time assert ( - codebuild.projects[0].buildspec + codebuild.projects[project_arn].buildspec == "arn:aws:s3:::my-codebuild-sample2/buildspec.yml" ) + + def test_codebuild_service(self): + codebuild = Codebuild(set_mocked_aws_audit_info()) + + assert codebuild.session.__class__.__name__ == "Session" + assert codebuild.service == "codebuild" + + assert len(codebuild.projects) == 1 + assert isinstance(codebuild.projects, dict) + assert isinstance(codebuild.projects[project_arn], Project) + assert codebuild.projects[project_arn].name == project_name + assert codebuild.projects[project_arn].arn == project_arn + assert codebuild.projects[project_arn].region == AWS_REGION_EU_WEST_1 + assert codebuild.projects[project_arn].last_invoked_time == last_invoked_time + assert codebuild.projects[project_arn].last_build == Build(id=build_id) + assert codebuild.projects[project_arn].buildspec == build_spec_project_arn diff --git a/tests/providers/aws/services/ec2/ec2_instance_managed_by_ssm/ec2_instance_managed_by_ssm_test.py b/tests/providers/aws/services/ec2/ec2_instance_managed_by_ssm/ec2_instance_managed_by_ssm_test.py index 9d17ec4de7a..82d2c7418eb 100644 --- a/tests/providers/aws/services/ec2/ec2_instance_managed_by_ssm/ec2_instance_managed_by_ssm_test.py +++ b/tests/providers/aws/services/ec2/ec2_instance_managed_by_ssm/ec2_instance_managed_by_ssm_test.py @@ -157,3 +157,177 @@ def test_ec2_instance_managed_by_ssm_compliance_instance(self): == f"EC2 Instance {instance.id} is managed by Systems Manager." ) assert result[0].resource_id == instance.id + + @mock_aws + def test_ec2_instance_managed_by_ssm_running(self): + ec2 = resource("ec2", region_name=AWS_REGION_US_EAST_1) + instances_pending = ec2.create_instances( + ImageId=EXAMPLE_AMI_ID, + MinCount=2, + MaxCount=2, + UserData="This is some user_data", + ) + instance_managed = ec2.Instance(instances_pending[0].id) + instance_unmanaged = ec2.Instance(instances_pending[1].id) + assert instance_managed.state["Name"] == "running" + assert instance_unmanaged.state["Name"] == "running" + + ssm_client = mock.MagicMock + ssm_client.managed_instances = { + instance_managed.id: ManagedInstance( + arn=f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:instance/{instance_managed.id}", + id=instance_managed.id, + region=AWS_REGION_US_EAST_1, + ) + } + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_audit_info( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ) + + with mock.patch( + "prowler.providers.aws.lib.audit_info.audit_info.current_audit_info", + new=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ssm.ssm_service.SSM", + new=ssm_client, + ), mock.patch( + "prowler.providers.aws.services.ssm.ssm_client.ssm_client", + new=ssm_client, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_instance_managed_by_ssm.ec2_instance_managed_by_ssm.ec2_client", + new=EC2(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_instance_managed_by_ssm.ec2_instance_managed_by_ssm import ( + ec2_instance_managed_by_ssm, + ) + + check = ec2_instance_managed_by_ssm() + results = check.execute() + + assert len(results) == 2 + for result in results: + if result.resource_id == instance_managed.id: + assert result.status == "PASS" + assert result.region == AWS_REGION_US_EAST_1 + assert result.resource_tags is None + assert ( + result.status_extended + == f"EC2 Instance {instance_managed.id} is managed by Systems Manager." + ) + + if result.resource_id == instance_unmanaged.id: + assert result.status == "FAIL" + assert result.region == AWS_REGION_US_EAST_1 + assert result.resource_tags is None + assert ( + result.status_extended + == f"EC2 Instance {instance_unmanaged.id} is not managed by Systems Manager." + ) + + @mock_aws + def test_ec2_instance_managed_by_ssm_stopped(self): + ec2 = resource("ec2", region_name=AWS_REGION_US_EAST_1) + instances_pending = ec2.create_instances( + ImageId=EXAMPLE_AMI_ID, + MinCount=1, + MaxCount=1, + UserData="This is some user_data", + ) + instances_pending[0].stop() + instance = ec2.Instance(instances_pending[0].id) + assert instance.state["Name"] == "stopped" + + ssm_client = mock.MagicMock + ssm_client.managed_instances = {} + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_audit_info( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ) + + with mock.patch( + "prowler.providers.aws.lib.audit_info.audit_info.current_audit_info", + new=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ssm.ssm_service.SSM", + new=ssm_client, + ), mock.patch( + "prowler.providers.aws.services.ssm.ssm_client.ssm_client", + new=ssm_client, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_instance_managed_by_ssm.ec2_instance_managed_by_ssm.ec2_client", + new=EC2(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_instance_managed_by_ssm.ec2_instance_managed_by_ssm import ( + ec2_instance_managed_by_ssm, + ) + + check = ec2_instance_managed_by_ssm() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags is None + assert ( + result[0].status_extended + == f"EC2 Instance {instance.id} is unmanaged by Systems Manager because it is stopped." + ) + + @mock_aws + def test_ec2_instance_managed_by_ssm_terminated(self): + ec2 = resource("ec2", region_name=AWS_REGION_US_EAST_1) + instances_pending = ec2.create_instances( + ImageId=EXAMPLE_AMI_ID, + MinCount=1, + MaxCount=1, + UserData="This is some user_data", + ) + instances_pending[0].terminate() + instance = ec2.Instance(instances_pending[0].id) + assert instance.state["Name"] == "terminated" + + ssm_client = mock.MagicMock + ssm_client.managed_instances = {} + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_audit_info( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ) + + with mock.patch( + "prowler.providers.aws.lib.audit_info.audit_info.current_audit_info", + new=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ssm.ssm_service.SSM", + new=ssm_client, + ), mock.patch( + "prowler.providers.aws.services.ssm.ssm_client.ssm_client", + new=ssm_client, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_instance_managed_by_ssm.ec2_instance_managed_by_ssm.ec2_client", + new=EC2(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_instance_managed_by_ssm.ec2_instance_managed_by_ssm import ( + ec2_instance_managed_by_ssm, + ) + + check = ec2_instance_managed_by_ssm() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags is None + assert ( + result[0].status_extended + == f"EC2 Instance {instance.id} is unmanaged by Systems Manager because it is terminated." + ) diff --git a/tests/providers/aws/services/iam/lib/policy_test.py b/tests/providers/aws/services/iam/lib/policy_test.py new file mode 100644 index 00000000000..98ec6381865 --- /dev/null +++ b/tests/providers/aws/services/iam/lib/policy_test.py @@ -0,0 +1,180 @@ +from prowler.providers.aws.services.iam.lib.policy import ( + is_condition_restricting_from_private_ip, + is_policy_cross_account, + is_policy_public, +) +from tests.providers.aws.audit_info_utils import AWS_ACCOUNT_NUMBER + + +class Test_Policy: + def test_is_policy_cross_account(self): + policy1 = { + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": ["arn:aws:iam::123456789012:root", "*"]}, + "Action": "s3:*", + "Resource": "arn:aws:s3:::example_bucket/*", + } + ] + } + policy2 = { + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": ["arn:aws:iam::123456789012:root"]}, + "Action": "s3:*", + "Resource": "arn:aws:s3:::example_bucket/*", + } + ] + } + policy3 = { + "Statement": [ + { + "Effect": "Deny", + "Principal": {"AWS": ["arn:aws:iam::123456789012:root"]}, + "Action": "s3:*", + "Resource": "arn:aws:s3:::example_bucket/*", + } + ] + } + + assert is_policy_cross_account(policy1, AWS_ACCOUNT_NUMBER) + assert not is_policy_cross_account(policy2, AWS_ACCOUNT_NUMBER) + assert not is_policy_cross_account(policy3, AWS_ACCOUNT_NUMBER) + + def test_is_policy_public(self): + policy1 = { + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "s3:*", + "Resource": "arn:aws:s3:::example_bucket/*", + } + ] + } + policy2 = { + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": "*"}, + "Action": "s3:*", + "Resource": "arn:aws:s3:::example_bucket/*", + } + ] + } + policy3 = { + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": "arn:aws:iam::123456789012:root"}, + "Action": "s3:*", + "Resource": "arn:aws:s3:::example_bucket/*", + } + ] + } + policy4 = { + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": "*"}, + "Action": "s3:*", + "Resource": "arn:aws:s3:::example_bucket/*", + "Condition": {"IpAddress": {"aws:SourceIp": "192.0.2.0/24"}}, + } + ] + } + + assert is_policy_public(policy1) + assert is_policy_public(policy2) + assert not is_policy_public(policy3) + assert not is_policy_public(policy4) + + def test_is_condition_restricting_from_private_ip_no_condition(self): + assert not is_condition_restricting_from_private_ip({}) + + def test_is_condition_restricting_from_private_ip(self): + condition_from_private_ip = { + "IpAddress": {"aws:SourceIp": "10.0.0.22"}, + } + assert is_condition_restricting_from_private_ip(condition_from_private_ip) + + def test_is_condition_restricting_from_public_ip(self): + condition_not_from_private_ip = { + "IpAddress": {"aws:SourceIp": "1.2.3.4"}, + } + assert not is_condition_restricting_from_private_ip( + condition_not_from_private_ip + ) + + def test_is_condition_restricting_from_private_ipv6(self): + condition_from_private_ipv6 = { + "IpAddress": {"aws:SourceIp": "fd00::1"}, + } + assert is_condition_restricting_from_private_ip(condition_from_private_ipv6) + + def test_is_condition_restricting_from_public_ipv6(self): + condition_not_from_private_ipv6 = { + "IpAddress": {"aws:SourceIp": "2001:0db8::1"}, + } + assert is_condition_restricting_from_private_ip(condition_not_from_private_ipv6) + + def test_is_condition_restricting_from_private_ip_network(self): + condition_from_private_ip_network = { + "IpAddress": {"aws:SourceIp": "10.0.0.0/24"}, + } + assert is_condition_restricting_from_private_ip( + condition_from_private_ip_network + ) + + def test_is_condition_restricting_from_public_ip_network(self): + condition_from_public_ip_network = { + "IpAddress": {"aws:SourceIp": "1.2.3.0/24"}, + } + + assert not is_condition_restricting_from_private_ip( + condition_from_public_ip_network + ) + + def test_is_condition_restricting_from_private_ipv6_network(self): + condition_from_private_ipv6_network = { + "IpAddress": {"aws:SourceIp": "fd00::/8"}, + } + assert is_condition_restricting_from_private_ip( + condition_from_private_ipv6_network + ) + + def test_is_condition_restricting_from_private_ip_array(self): + condition_from_private_ip_array = { + "IpAddress": {"aws:SourceIp": ["10.0.0.22", "192.168.1.1"]}, + } + assert is_condition_restricting_from_private_ip(condition_from_private_ip_array) + + def test_is_condition_restricting_from_private_ipv6_array(self): + condition_from_private_ipv6_array = { + "IpAddress": {"aws:SourceIp": ["fd00::1", "fe80::1"]}, + } + assert is_condition_restricting_from_private_ip( + condition_from_private_ipv6_array + ) + + def test_is_condition_restricting_from_mixed_ip_array(self): + condition_from_mixed_ip_array = { + "IpAddress": {"aws:SourceIp": ["10.0.0.22", "2001:0db8::1"]}, + } + assert is_condition_restricting_from_private_ip(condition_from_mixed_ip_array) + + def test_is_condition_restricting_from_mixed_ip_array_not_private(self): + condition_from_mixed_ip_array_not_private = { + "IpAddress": {"aws:SourceIp": ["1.2.3.4", "2001:0db8::1"]}, + } + assert not is_condition_restricting_from_private_ip( + condition_from_mixed_ip_array_not_private + ) + + def test_is_condition_restricting_from_private_ip_from_invalid_ip(self): + condition_from_invalid_ip = { + "IpAddress": {"aws:SourceIp": "256.256.256.256"}, + } + assert not is_condition_restricting_from_private_ip(condition_from_invalid_ip) diff --git a/tests/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access_test.py b/tests/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access_test.py index 1e0b5919ea4..a0be70f8e11 100644 --- a/tests/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access_test.py +++ b/tests/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access_test.py @@ -350,35 +350,312 @@ def test_bucket_public_policy(self): ) assert result[0].region == AWS_REGION_US_EAST_1 + @mock_aws + def test_bucket_not_public_due_to_policy_conditions_from_vpc(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + # Generate S3Control Client + s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1) + s3control_client.put_public_access_block( + AccountId=AWS_ACCOUNT_NUMBER, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_public_access_block( + Bucket=bucket_name_us, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + public_write_policy = '{"Version": "2012-10-17","Id": "PutObjPolicy","Statement": [{"Sid": "PublicWritePolicy","Effect": "Allow","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket_test_us/*","Condition": {"StringEquals": {"aws:SourceVpc": "vpc-123456"}}}]}' + s3_client.put_bucket_policy( + Bucket=bucket_name_us, + Policy=public_write_policy, + ) + from prowler.providers.aws.services.s3.s3_service import S3, S3Control + + aws_provider = set_mocked_aws_audit_info([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.aws.lib.audit_info.audit_info.current_audit_info", + new=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access.s3_client", + new=S3(aws_provider), + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access.s3control_client", + new=S3Control(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access import ( + s3_bucket_public_access, + ) + + check = s3_bucket_public_access() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"S3 Bucket {bucket_name_us} is not public." + ) + assert result[0].resource_id == bucket_name_us + assert ( + result[0].resource_arn + == f"arn:{aws_provider.audited_partition}:s3:::{bucket_name_us}" + ) + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_bucket_not_public_due_to_policy_conditions_from_private_ip(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + # Generate S3Control Client + s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1) + s3control_client.put_public_access_block( + AccountId=AWS_ACCOUNT_NUMBER, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_public_access_block( + Bucket=bucket_name_us, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + public_write_policy = '{"Version": "2012-10-17","Id": "PutObjPolicy","Statement": [{"Sid": "PublicWritePolicy","Effect": "Allow","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket_test_us/*","Condition": {"IpAddress": {"aws:SourceIp": "10.0.0.25"}}}]}' + s3_client.put_bucket_policy( + Bucket=bucket_name_us, + Policy=public_write_policy, + ) + from prowler.providers.aws.services.s3.s3_service import S3, S3Control + + aws_provider = set_mocked_aws_audit_info([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.aws.lib.audit_info.audit_info.current_audit_info", + new=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access.s3_client", + new=S3(aws_provider), + ), mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access.s3control_client", + new=S3Control(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access import ( + s3_bucket_public_access, + ) + + check = s3_bucket_public_access() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"S3 Bucket {bucket_name_us} is not public." + ) + assert result[0].resource_id == bucket_name_us + assert ( + result[0].resource_arn + == f"arn:{aws_provider.audited_partition}:s3:::{bucket_name_us}" + ) + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_bucket_public_due_to_policy_conditions_from_public_ip(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + # Generate S3Control Client + s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1) + s3control_client.put_public_access_block( + AccountId=AWS_ACCOUNT_NUMBER, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_public_access_block( + Bucket=bucket_name_us, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + public_write_policy = '{"Version": "2012-10-17","Id": "PutObjPolicy","Statement": [{"Sid": "PublicWritePolicy","Effect": "Allow","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket_test_us/*","Condition": {"IpAddress": {"aws:SourceIp": "1.2.3.4"}}}]}' + s3_client.put_bucket_policy( + Bucket=bucket_name_us, + Policy=public_write_policy, + ) + from prowler.providers.aws.services.s3.s3_service import S3, S3Control + + aws_provider = set_mocked_aws_audit_info([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.aws.lib.audit_info.audit_info.current_audit_info", + new=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access.s3_client", + new=S3(aws_provider), + ), mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access.s3control_client", + new=S3Control(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access import ( + s3_bucket_public_access, + ) + + check = s3_bucket_public_access() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"S3 Bucket {bucket_name_us} has public access due to bucket policy." + ) + assert result[0].resource_id == bucket_name_us + assert ( + result[0].resource_arn + == f"arn:{aws_provider.audited_partition}:s3:::{bucket_name_us}" + ) + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_bucket_public_due_to_policy_conditions_from_public_and_private_ips(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + # Generate S3Control Client + s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1) + s3control_client.put_public_access_block( + AccountId=AWS_ACCOUNT_NUMBER, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_public_access_block( + Bucket=bucket_name_us, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + public_write_policy = '{"Version": "2012-10-17","Id": "PutObjPolicy","Statement": [{"Sid": "PublicWritePolicy","Effect": "Allow","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket_test_us/*","Condition": {"IpAddress": {"aws:SourceIp": ["192.168.10.0/24", "2001:DB8:1234:5678::/64", "1.2.3.4"]}}}]}' + s3_client.put_bucket_policy( + Bucket=bucket_name_us, + Policy=public_write_policy, + ) + from prowler.providers.aws.services.s3.s3_service import S3, S3Control + + aws_provider = set_mocked_aws_audit_info([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.aws.lib.audit_info.audit_info.current_audit_info", + new=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access.s3_client", + new=S3(aws_provider), + ), mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access.s3control_client", + new=S3Control(aws_provider), + ): + from prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access import ( + s3_bucket_public_access, + ) + + check = s3_bucket_public_access() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"S3 Bucket {bucket_name_us} has public access due to bucket policy." + ) + assert result[0].resource_id == bucket_name_us + assert ( + result[0].resource_arn + == f"arn:{aws_provider.audited_partition}:s3:::{bucket_name_us}" + ) + assert result[0].region == AWS_REGION_US_EAST_1 + @mock_aws def test_bucket_not_public(self): s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) bucket_name_us = "bucket_test_us" s3_client.create_bucket(Bucket=bucket_name_us) + # Generate S3Control Client + s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1) + s3control_client.put_public_access_block( + AccountId=AWS_ACCOUNT_NUMBER, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) s3_client.put_public_access_block( Bucket=bucket_name_us, PublicAccessBlockConfiguration={ - "BlockPublicAcls": True, - "IgnorePublicAcls": True, - "BlockPublicPolicy": True, - "RestrictPublicBuckets": True, + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, }, ) + public_write_policy = '{"Version": "2012-10-17","Id": "PutObjPolicy","Statement": [{"Sid": "PublicWritePolicy","Effect": "Allow","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket_test_us/*","Condition": {"StringEquals": {"aws:SourceVpc": "vpc-123456"}}}]}' + s3_client.put_bucket_policy( + Bucket=bucket_name_us, + Policy=public_write_policy, + ) from prowler.providers.aws.services.s3.s3_service import S3, S3Control - audit_info = set_mocked_aws_audit_info([AWS_REGION_US_EAST_1]) + aws_provider = set_mocked_aws_audit_info([AWS_REGION_US_EAST_1]) with mock.patch( "prowler.providers.aws.lib.audit_info.audit_info.current_audit_info", - new=audit_info, + new=aws_provider, ): with mock.patch( "prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access.s3_client", - new=S3(audit_info), + new=S3(aws_provider), ): with mock.patch( "prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access.s3control_client", - new=S3Control(audit_info), + new=S3Control(aws_provider), ): # Test Check from prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access import ( @@ -390,14 +667,14 @@ def test_bucket_not_public(self): assert len(result) == 1 assert result[0].status == "PASS" - assert search( - "not public", - result[0].status_extended, + assert ( + result[0].status_extended + == f"S3 Bucket {bucket_name_us} is not public." ) assert result[0].resource_id == bucket_name_us assert ( result[0].resource_arn - == f"arn:{audit_info.audited_partition}:s3:::{bucket_name_us}" + == f"arn:{aws_provider.audited_partition}:s3:::{bucket_name_us}" ) assert result[0].region == AWS_REGION_US_EAST_1 @@ -443,3 +720,70 @@ def test_bucket_can_not_retrieve_public_access_block(self): result = check.execute() assert len(result) == 0 + + @mock_aws + def test_bucket_public_with_aws_principals(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + # Generate S3Control Client + s3control_client = client("s3control", region_name=AWS_REGION_US_EAST_1) + s3control_client.put_public_access_block( + AccountId=AWS_ACCOUNT_NUMBER, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + s3_client.put_public_access_block( + Bucket=bucket_name_us, + PublicAccessBlockConfiguration={ + "BlockPublicAcls": False, + "IgnorePublicAcls": False, + "BlockPublicPolicy": False, + "RestrictPublicBuckets": False, + }, + ) + public_write_policy = '{"Version": "2012-10-17","Id": "PutObjPolicy","Statement": [{"Sid": "PublicWritePolicy","Effect": "Allow","Principal": {"AWS": ["arn:aws:iam::123456789012:root", "*"]},"Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket_test_us/*"}]}' + s3_client.put_bucket_policy( + Bucket=bucket_name_us, + Policy=public_write_policy, + ) + from prowler.providers.aws.services.s3.s3_service import S3, S3Control + + aws_provider = set_mocked_aws_audit_info([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.aws.lib.audit_info.audit_info.current_audit_info", + new=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access.s3_client", + new=S3(aws_provider), + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access.s3control_client", + new=S3Control(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.s3.s3_bucket_public_access.s3_bucket_public_access import ( + s3_bucket_public_access, + ) + + check = s3_bucket_public_access() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"S3 Bucket {bucket_name_us} has public access due to bucket policy." + ) + assert result[0].resource_id == bucket_name_us + assert ( + result[0].resource_arn + == f"arn:{aws_provider.audited_partition}:s3:::{bucket_name_us}" + ) + assert result[0].region == AWS_REGION_US_EAST_1 diff --git a/tests/providers/aws/services/s3/s3_bucket_secure_transport_policy/s3_bucket_secure_transport_policy_test.py b/tests/providers/aws/services/s3/s3_bucket_secure_transport_policy/s3_bucket_secure_transport_policy_test.py index d52a4134af5..74e9c838dcc 100644 --- a/tests/providers/aws/services/s3/s3_bucket_secure_transport_policy/s3_bucket_secure_transport_policy_test.py +++ b/tests/providers/aws/services/s3/s3_bucket_secure_transport_policy/s3_bucket_secure_transport_policy_test.py @@ -175,3 +175,65 @@ def test_bucket_uncomply_policy(self): == f"arn:{audit_info.audited_partition}:s3:::{bucket_name_us}" ) assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_bucket_uncomply_policy_without_action(self): + s3_client_us_east_1 = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client_us_east_1.create_bucket(Bucket=bucket_name_us) + + ssl_policy = """ +{ + "Version": "2012-10-17", + "Id": "PutObjPolicy", + "Statement": [ + { + "Sid": "s3-bucket-ssl-requests-only", + "Effect": "Deny", + "Principal": "*", + "Resource": "arn:aws:s3:::bucket_test_us/*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + } + } + ] +} +""" + s3_client_us_east_1.put_bucket_policy( + Bucket=bucket_name_us, + Policy=ssl_policy, + ) + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_audit_info([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.aws.lib.audit_info.audit_info.current_audit_info", + new=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_secure_transport_policy.s3_bucket_secure_transport_policy.s3_client", + new=S3(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.s3.s3_bucket_secure_transport_policy.s3_bucket_secure_transport_policy import ( + s3_bucket_secure_transport_policy, + ) + + check = s3_bucket_secure_transport_policy() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"S3 Bucket {bucket_name_us} allows requests over insecure transport in the bucket policy." + ) + assert result[0].resource_id == bucket_name_us + assert ( + result[0].resource_arn + == f"arn:{aws_provider.audited_partition}:s3:::{bucket_name_us}" + ) + assert result[0].region == AWS_REGION_US_EAST_1 diff --git a/tests/providers/azure/azure_fixtures.py b/tests/providers/azure/azure_fixtures.py index 55741defff3..b745f8060f9 100644 --- a/tests/providers/azure/azure_fixtures.py +++ b/tests/providers/azure/azure_fixtures.py @@ -9,6 +9,7 @@ ) AZURE_SUBSCRIPTION = str(uuid4()) +AZURE_SUBSCRIPTION_ID = str(uuid4()) # Azure Identity IDENTITY_ID = "00000000-0000-0000-0000-000000000000" diff --git a/tests/providers/azure/services/network/network_watcher_enabled/network_watcher_enabled_test.py b/tests/providers/azure/services/network/network_watcher_enabled/network_watcher_enabled_test.py index d4bef4046a5..acc9430fe0f 100644 --- a/tests/providers/azure/services/network/network_watcher_enabled/network_watcher_enabled_test.py +++ b/tests/providers/azure/services/network/network_watcher_enabled/network_watcher_enabled_test.py @@ -1,7 +1,10 @@ from unittest import mock from prowler.providers.azure.services.network.network_service import NetworkWatcher -from tests.providers.azure.azure_fixtures import AZURE_SUBSCRIPTION +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION, + AZURE_SUBSCRIPTION_ID, +) class Test_network_watcher_enabled: @@ -31,8 +34,9 @@ def test_network_invalid_network_watchers(self): network_client = mock.MagicMock locations = ["location"] network_client.locations = {AZURE_SUBSCRIPTION: locations} + network_client.subscriptions = {AZURE_SUBSCRIPTION: AZURE_SUBSCRIPTION_ID} network_watcher_name = "Network Watcher" - network_watcher_id = f"/subscriptions/{AZURE_SUBSCRIPTION}/providers/Microsoft.Network/networkWatchers/{locations[0]}" + network_watcher_id = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/resourceGroups/NetworkWatcherRG/providers/Microsoft.Network/networkWatchers/NetworkWatcher_*" network_client.network_watchers = { AZURE_SUBSCRIPTION: [ @@ -62,18 +66,20 @@ def test_network_invalid_network_watchers(self): assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Network Watcher is not enabled for the location {locations[0]} in subscription {AZURE_SUBSCRIPTION}." + == f"Network Watcher is not enabled for the following locations in subscription '{AZURE_SUBSCRIPTION}': location." ) assert result[0].subscription == AZURE_SUBSCRIPTION assert result[0].resource_name == network_watcher_name assert result[0].resource_id == network_watcher_id + assert result[0].location == "Global" def test_network_valid_network_watchers(self): network_client = mock.MagicMock locations = ["location"] network_client.locations = {AZURE_SUBSCRIPTION: locations} + network_client.subscriptions = {AZURE_SUBSCRIPTION: AZURE_SUBSCRIPTION_ID} network_watcher_name = "Network Watcher" - network_watcher_id = f"/subscriptions/{AZURE_SUBSCRIPTION}/providers/Microsoft.Network/networkWatchers/{locations[0]}" + network_watcher_id = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/resourceGroups/NetworkWatcherRG/providers/Microsoft.Network/networkWatchers/NetworkWatcher_*" network_client.network_watchers = { AZURE_SUBSCRIPTION: [ @@ -103,8 +109,9 @@ def test_network_valid_network_watchers(self): assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Network Watcher is enabled for the location {locations[0]} in subscription {AZURE_SUBSCRIPTION}." + == f"Network Watcher is enabled for all locations in subscription '{AZURE_SUBSCRIPTION}'." ) assert result[0].subscription == AZURE_SUBSCRIPTION assert result[0].resource_name == network_watcher_name assert result[0].resource_id == network_watcher_id + assert result[0].location == "Global"