From 0b238243b117209b330d9e7781676042237a256d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20De=20la=20Torre=20Vico?= Date: Thu, 22 Aug 2024 17:08:49 +0200 Subject: [PATCH] feat(elbv2): add new check `elbv2_is_in_multiple_az` (#4800) Co-authored-by: Sergio Garcia <38561120+sergargar@users.noreply.github.com> --- docs/tutorials/configuration_file.md | 3 +- prowler/config/config.yaml | 5 + .../elbv2/elbv2_is_in_multiple_az/__init__.py | 0 .../elbv2_is_in_multiple_az.metadata.json | 30 ++++ .../elbv2_is_in_multiple_az.py | 26 +++ .../aws/services/elbv2/elbv2_service.py | 6 + tests/config/config_test.py | 1 + tests/config/fixtures/config.yaml | 5 + .../elbv2_is_in_multiple_az_test.py | 150 ++++++++++++++++++ .../aws/services/elbv2/elbv2_service_test.py | 13 ++ 10 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 prowler/providers/aws/services/elbv2/elbv2_is_in_multiple_az/__init__.py create mode 100644 prowler/providers/aws/services/elbv2/elbv2_is_in_multiple_az/elbv2_is_in_multiple_az.metadata.json create mode 100644 prowler/providers/aws/services/elbv2/elbv2_is_in_multiple_az/elbv2_is_in_multiple_az.py create mode 100644 tests/providers/aws/services/elbv2/elbv2_is_in_multiple_az/elbv2_is_in_multiple_az_test.py diff --git a/docs/tutorials/configuration_file.md b/docs/tutorials/configuration_file.md index 1040a303cc0..15f5beac4d7 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 3d7a03c3bef..ecf71ecbf6d 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 00000000000..e69de29bb2d 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 00000000000..1ef2cbf5089 --- /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 00000000000..46dd5e62502 --- /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 dd3626dd3a7..d0693b38d9d 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 f446087f52d..18fc432fca0 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 cdad3215696..365619a4760 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 00000000000..ab4f505898a --- /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 cb7bd517106..81ab1d31ebd 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