diff --git a/docs/tutorials/configuration_file.md b/docs/tutorials/configuration_file.md index 1040a303cc..15f5beac4d 100644 --- a/docs/tutorials/configuration_file.md +++ b/docs/tutorials/configuration_file.md @@ -46,7 +46,8 @@ The following list includes all the AWS checks with configurable variables that | `ec2_securitygroup_allow_ingress_from_internet_to_any_port` | `ec2_allowed_instance_owners` | List of Strings | | `acm_certificates_expiration_check` | `days_to_expire_threshold` | Integer | | `eks_control_plane_logging_all_types_enabled` | `eks_required_log_types` | List of Strings | -| `eks_cluster_uses_a_supported_version` | `eks_cluster_oldest_version_supported` | String | +| `eks_cluster_uses_a_supported_version` | `eks_cluster_oldest_version_supported` | String | +| `elbv2_is_in_multiple_az` | `elbv2_min_azs` | Integer | ## Azure diff --git a/prowler/config/config.yaml b/prowler/config/config.yaml index 3d7a03c3be..ecf71ecbf6 100644 --- a/prowler/config/config.yaml +++ b/prowler/config/config.yaml @@ -316,6 +316,11 @@ aws: ] + # AWS ELBv2 Configuration + # aws.elbv2_is_in_multiple_az + # Minimum number of Availability Zones that an ELBv2 must be in + elbv2_min_azs: 2 + # Azure Configuration azure: # Azure Network Configuration diff --git a/prowler/providers/aws/services/elbv2/elbv2_is_in_multiple_az/__init__.py b/prowler/providers/aws/services/elbv2/elbv2_is_in_multiple_az/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/elbv2/elbv2_is_in_multiple_az/elbv2_is_in_multiple_az.metadata.json b/prowler/providers/aws/services/elbv2/elbv2_is_in_multiple_az/elbv2_is_in_multiple_az.metadata.json new file mode 100644 index 0000000000..1ef2cbf508 --- /dev/null +++ b/prowler/providers/aws/services/elbv2/elbv2_is_in_multiple_az/elbv2_is_in_multiple_az.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "aws", + "CheckID": "elbv2_is_in_multiple_az", + "CheckTitle": "Elastic Load Balancer V2 (ELBv2) is Configured Across Multiple Availability Zones (AZs)", + "CheckType": [], + "ServiceName": "elbv2", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "Severity": "medium", + "ResourceType": "AwsElasticLoadBalancingV2LoadBalancer", + "Description": "Ensure whether Elastic Load Balancer V2 (Application, Network, or Gateway Load Balancer) is configured to operate across multiple Availability Zones (AZs). Ensuring that your load balancer is spread across at least two AZs helps maintain high availability and fault tolerance in case of an AZ failure.", + "Risk": "If an ELBv2 is not configured across multiple AZs, there is a risk that an Availability Zone failure could lead to downtime for your application. This could result in a single point of failure, impacting the availability and reliability of your services.", + "RelatedUrl": "https://docs.aws.amazon.com/elasticloadbalancing/latest/userguide/how-elastic-load-balancing-works.html#availability-zones", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/elb-controls.html#elb-13", + "Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/ELBv2/enable-multi-az.html" + }, + "Recommendation": { + "Text": "It is recommended to configure your ELBv2 to operate across at least two Availability Zones to enhance fault tolerance and availability.", + "Url": "https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-subnets.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/elbv2/elbv2_is_in_multiple_az/elbv2_is_in_multiple_az.py b/prowler/providers/aws/services/elbv2/elbv2_is_in_multiple_az/elbv2_is_in_multiple_az.py new file mode 100644 index 0000000000..46dd5e6250 --- /dev/null +++ b/prowler/providers/aws/services/elbv2/elbv2_is_in_multiple_az/elbv2_is_in_multiple_az.py @@ -0,0 +1,26 @@ +from typing import List + +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.elbv2.elbv2_client import elbv2_client + + +class elbv2_is_in_multiple_az(Check): + def execute(self) -> List[Check_Report_AWS]: + findings = [] + elbv2_min_azs = elbv2_client.audit_config.get("elbv2_min_azs", 2) + for load_balancer_arn, load_balancer in elbv2_client.loadbalancersv2.items(): + report = Check_Report_AWS(self.metadata()) + report.region = load_balancer.region + report.resource_id = load_balancer.name + report.resource_arn = load_balancer_arn + report.resource_tags = load_balancer.tags + report.status = "FAIL" + report.status_extended = f"ELBv2 {load_balancer.name} is not in at least {elbv2_min_azs} AZs. Is only in {', '.join(load_balancer.availability_zones.keys())}." + + if len(load_balancer.availability_zones) >= elbv2_min_azs: + report.status = "PASS" + report.status_extended = f"ELBv2 {load_balancer.name} is at least in {elbv2_min_azs} AZs: {', '.join(load_balancer.availability_zones.keys())}." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/elbv2/elbv2_service.py b/prowler/providers/aws/services/elbv2/elbv2_service.py index dd3626dd3a..d0693b38d9 100644 --- a/prowler/providers/aws/services/elbv2/elbv2_service.py +++ b/prowler/providers/aws/services/elbv2/elbv2_service.py @@ -47,6 +47,10 @@ def _describe_load_balancers(self, regional_client): type=elbv2["Type"], dns=elbv2.get("DNSName", None), scheme=elbv2.get("Scheme", None), + availability_zones={ + az["ZoneName"]: az["SubnetId"] + for az in elbv2.get("AvailabilityZones", []) + }, ) except Exception as error: logger.error( @@ -200,4 +204,6 @@ class LoadBalancerv2(BaseModel): drop_invalid_header_fields: Optional[str] listeners: Dict[str, Listenerv2] = {} scheme: Optional[str] + # Key: ZoneName, Value: SubnetId + availability_zones: Dict[str, str] = {} tags: Optional[list] = [] diff --git a/tests/config/config_test.py b/tests/config/config_test.py index f446087f52..18fc432fca 100644 --- a/tests/config/config_test.py +++ b/tests/config/config_test.py @@ -288,6 +288,7 @@ def mock_prowler_get_latest_release(_, **kwargs): ], "eks_cluster_oldest_version_supported": "1.28", "excluded_sensitive_environment_variables": [], + "elbv2_min_azs": 2, } config_azure = { diff --git a/tests/config/fixtures/config.yaml b/tests/config/fixtures/config.yaml index cdad321569..365619a476 100644 --- a/tests/config/fixtures/config.yaml +++ b/tests/config/fixtures/config.yaml @@ -315,6 +315,11 @@ aws: ] + # AWS ELBv2 Configuration + # aws.elbv2_is_in_multiple_az + # Minimum number of Availability Zones that an ELBv2 must be in + elbv2_min_azs: 2 + # Azure Configuration azure: # Azure Network Configuration diff --git a/tests/providers/aws/services/elbv2/elbv2_is_in_multiple_az/elbv2_is_in_multiple_az_test.py b/tests/providers/aws/services/elbv2/elbv2_is_in_multiple_az/elbv2_is_in_multiple_az_test.py new file mode 100644 index 0000000000..ab4f505898 --- /dev/null +++ b/tests/providers/aws/services/elbv2/elbv2_is_in_multiple_az/elbv2_is_in_multiple_az_test.py @@ -0,0 +1,150 @@ +from unittest import mock + +from boto3 import client, resource +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_REGION_EU_WEST_1, + AWS_REGION_EU_WEST_1_AZA, + AWS_REGION_EU_WEST_1_AZB, + set_mocked_aws_provider, +) + + +class Test_elbv2_is_in_multiple_az: + @mock_aws + def test_no_elbs(self): + from prowler.providers.aws.services.elbv2.elbv2_service import ELBv2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.elbv2.elbv2_is_in_multiple_az.elbv2_is_in_multiple_az.elbv2_client", + new=ELBv2(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.elbv2.elbv2_is_in_multiple_az.elbv2_is_in_multiple_az import ( + elbv2_is_in_multiple_az, + ) + + check = elbv2_is_in_multiple_az() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_elbv2_in_one_avaibility_zone(self): + # Create VPC, Subnets and Security Group + elbv2_client = client("elbv2", region_name=AWS_REGION_EU_WEST_1) + + ec2 = resource("ec2", region_name=AWS_REGION_EU_WEST_1) + + security_group = ec2.create_security_group( + GroupName="a-security-group", Description="First One" + ) + + vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16") + + subnet1 = ec2.create_subnet( + AvailabilityZone=AWS_REGION_EU_WEST_1_AZA, + CidrBlock="10.0.1.0/24", + VpcId=vpc.id, + ) + + lb_arn = elbv2_client.create_load_balancer( + Name="test_elbv2", + Subnets=[subnet1.id], + SecurityGroups=[security_group.id], + )["LoadBalancers"][0]["LoadBalancerArn"] + + from prowler.providers.aws.services.elbv2.elbv2_service import ELBv2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.elbv2.elbv2_is_in_multiple_az.elbv2_is_in_multiple_az.elbv2_client", + new=ELBv2(aws_provider), + ): + from prowler.providers.aws.services.elbv2.elbv2_is_in_multiple_az.elbv2_is_in_multiple_az import ( + elbv2_is_in_multiple_az, + ) + + check = elbv2_is_in_multiple_az() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"ELBv2 test_elbv2 is not in at least 2 AZs. Is only in {AWS_REGION_EU_WEST_1_AZA}." + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_id == "test_elbv2" + assert result[0].resource_arn == lb_arn + assert result[0].resource_tags == [] + + @mock_aws + def test_elbv2_in_two_avaibility_zones(self): + # Create VPC, Subnets and Security Group + elbv2_client = client("elbv2", region_name=AWS_REGION_EU_WEST_1) + + ec2 = resource("ec2", region_name=AWS_REGION_EU_WEST_1) + + security_group = ec2.create_security_group( + GroupName="a-security-group", Description="First One" + ) + + vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16") + + subnet1 = ec2.create_subnet( + AvailabilityZone=AWS_REGION_EU_WEST_1_AZA, + CidrBlock="10.0.1.0/24", + VpcId=vpc.id, + ) + + subnet2 = ec2.create_subnet( + AvailabilityZone=AWS_REGION_EU_WEST_1_AZB, + CidrBlock="10.0.2.0/24", + VpcId=vpc.id, + ) + + lb_arn = elbv2_client.create_load_balancer( + Name="test_elbv2", + Subnets=[subnet1.id, subnet2.id], + SecurityGroups=[security_group.id], + )["LoadBalancers"][0]["LoadBalancerArn"] + + from prowler.providers.aws.services.elbv2.elbv2_service import ELBv2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.elbv2.elbv2_is_in_multiple_az.elbv2_is_in_multiple_az.elbv2_client", + new=ELBv2(aws_provider), + ): + from prowler.providers.aws.services.elbv2.elbv2_is_in_multiple_az.elbv2_is_in_multiple_az import ( + elbv2_is_in_multiple_az, + ) + + check = elbv2_is_in_multiple_az() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"ELBv2 test_elbv2 is at least in 2 AZs: {AWS_REGION_EU_WEST_1_AZA}, {AWS_REGION_EU_WEST_1_AZB}." + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_id == "test_elbv2" + assert result[0].resource_arn == lb_arn + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/elbv2/elbv2_service_test.py b/tests/providers/aws/services/elbv2/elbv2_service_test.py index cb7bd51710..81ab1d31eb 100644 --- a/tests/providers/aws/services/elbv2/elbv2_service_test.py +++ b/tests/providers/aws/services/elbv2/elbv2_service_test.py @@ -88,6 +88,19 @@ def test_describe_load_balancers(self): elbv2.loadbalancersv2[lb["LoadBalancerArn"]].dns == "my-lb-1.eu-west-1.elb.amazonaws.com" ) + assert len(elbv2.loadbalancersv2[lb["LoadBalancerArn"]].availability_zones) == 2 + assert ( + elbv2.loadbalancersv2[lb["LoadBalancerArn"]].availability_zones[ + AWS_REGION_EU_WEST_1_AZA + ] + == subnet1.id + ) + assert ( + elbv2.loadbalancersv2[lb["LoadBalancerArn"]].availability_zones[ + AWS_REGION_EU_WEST_1_AZB + ] + == subnet2.id + ) # Test ELBv2 Describe Listeners @mock_aws