diff --git a/docs/tutorials/configuration_file.md b/docs/tutorials/configuration_file.md index 15f5beac4d..fb2b2b689c 100644 --- a/docs/tutorials/configuration_file.md +++ b/docs/tutorials/configuration_file.md @@ -48,6 +48,7 @@ The following list includes all the AWS checks with configurable variables that | `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 | | `elbv2_is_in_multiple_az` | `elbv2_min_azs` | Integer | +| `elb_is_in_multiple_az` | `elb_min_azs` | Integer | ## Azure diff --git a/prowler/config/config.yaml b/prowler/config/config.yaml index ecf71ecbf6..05fabd6ff5 100644 --- a/prowler/config/config.yaml +++ b/prowler/config/config.yaml @@ -316,6 +316,11 @@ aws: ] + # AWS ELB Configuration + # aws.elb_is_in_multiple_az + # Minimum number of Availability Zones that an CLB must be in + elb_min_azs: 2 + # AWS ELBv2 Configuration # aws.elbv2_is_in_multiple_az # Minimum number of Availability Zones that an ELBv2 must be in diff --git a/prowler/providers/aws/services/elb/elb_is_in_multiple_az/__init__.py b/prowler/providers/aws/services/elb/elb_is_in_multiple_az/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/elb/elb_is_in_multiple_az/elb_is_in_multiple_az.metadata.json b/prowler/providers/aws/services/elb/elb_is_in_multiple_az/elb_is_in_multiple_az.metadata.json new file mode 100644 index 0000000000..c8571a7d2a --- /dev/null +++ b/prowler/providers/aws/services/elb/elb_is_in_multiple_az/elb_is_in_multiple_az.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "aws", + "CheckID": "elb_is_in_multiple_az", + "CheckTitle": "Ensure Classic Load Balancer is Configured Across Multiple Availability Zones", + "CheckType": [], + "ServiceName": "elb", + "SubServiceName": "", + "ResourceIdTemplate": "arn::elasticloadbalancing:::loadbalancer/", + "Severity": "medium", + "ResourceType": "AwsElbLoadBalancer", + "Description": "This check ensures that a Classic Load Balancer is configured to span at least the specified number of Availability Zones (AZs). The control fails if the Load Balancer does not span multiple AZs, which can lead to decreased availability and reliability in case of an AZ failure.", + "Risk": "A Classic Load Balancer configured in a single Availability Zone risks becoming a single point of failure. If the AZ fails, the load balancer will not be able to redirect traffic to other healthy targets, leading to potential service outages.", + "RelatedUrl": "https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/introduction.html#classic-load-balancer-overview", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/elb-controls.html#elb-10", + "Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/ELB/ec2-instances-distribution-across-availability-zones.html" + }, + "Recommendation": { + "Text": "Distribute your Classic Load Balancer across multiple Availability Zones to improve redundancy and fault tolerance.", + "Url": "https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/enable-disable-crosszone-lb.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/elb/elb_is_in_multiple_az/elb_is_in_multiple_az.py b/prowler/providers/aws/services/elb/elb_is_in_multiple_az/elb_is_in_multiple_az.py new file mode 100644 index 0000000000..d8404382ca --- /dev/null +++ b/prowler/providers/aws/services/elb/elb_is_in_multiple_az/elb_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.elb.elb_client import elb_client + + +class elb_is_in_multiple_az(Check): + def execute(self) -> List[Check_Report_AWS]: + findings = [] + ELB_MIN_AZS = elb_client.audit_config.get("elb_min_azs", 2) + for loadbalancer_arn, load_balancer in elb_client.loadbalancers.items(): + report = Check_Report_AWS(self.metadata()) + report.region = load_balancer.region + report.resource_id = load_balancer.name + report.resource_arn = loadbalancer_arn + report.resource_tags = load_balancer.tags + report.status = "FAIL" + report.status_extended = f"Classic Load Balancer {load_balancer.name} is not in at least {ELB_MIN_AZS} availability zones, it is only in {', '.join(load_balancer.availability_zones)}." + + if len(load_balancer.availability_zones) >= ELB_MIN_AZS: + report.status = "PASS" + report.status_extended = f"Classic Load Balancer {load_balancer.name} is in {len(load_balancer.availability_zones)} availability zones: {', '.join(load_balancer.availability_zones)}." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/elb/elb_service.py b/prowler/providers/aws/services/elb/elb_service.py index b9c14a78dd..49b2494354 100644 --- a/prowler/providers/aws/services/elb/elb_service.py +++ b/prowler/providers/aws/services/elb/elb_service.py @@ -45,6 +45,7 @@ class ELB(AWSService): region=regional_client.region, scheme=elb["Scheme"], listeners=listeners, + availability_zones=set(elb.get("AvailabilityZones", [])), ) except Exception as error: logger.error( @@ -99,4 +100,5 @@ class LoadBalancer(BaseModel): access_logs: Optional[bool] listeners: list[Listener] cross_zone_load_balancing: Optional[bool] + availability_zones: set[str] tags: Optional[list] = [] diff --git a/tests/config/config_test.py b/tests/config/config_test.py index 18fc432fca..e3c9df7740 100644 --- a/tests/config/config_test.py +++ b/tests/config/config_test.py @@ -288,6 +288,7 @@ config_aws = { ], "eks_cluster_oldest_version_supported": "1.28", "excluded_sensitive_environment_variables": [], + "elb_min_azs": 2, "elbv2_min_azs": 2, } diff --git a/tests/config/fixtures/config.yaml b/tests/config/fixtures/config.yaml index 365619a476..7c04d52162 100644 --- a/tests/config/fixtures/config.yaml +++ b/tests/config/fixtures/config.yaml @@ -315,6 +315,11 @@ aws: ] + # AWS ELB Configuration + # aws.elb_is_in_multiple_az + # Minimum number of Availability Zones that an CLB must be in + elb_min_azs: 2 + # AWS ELBv2 Configuration # aws.elbv2_is_in_multiple_az # Minimum number of Availability Zones that an ELBv2 must be in diff --git a/tests/providers/aws/services/elb/elb_is_in_multiple_az/elb_is_in_multiple_az_test.py b/tests/providers/aws/services/elb/elb_is_in_multiple_az/elb_is_in_multiple_az_test.py new file mode 100644 index 0000000000..a47f2c2a1f --- /dev/null +++ b/tests/providers/aws/services/elb/elb_is_in_multiple_az/elb_is_in_multiple_az_test.py @@ -0,0 +1,131 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + AWS_REGION_EU_WEST_1_AZA, + AWS_REGION_EU_WEST_1_AZB, + set_mocked_aws_provider, +) + + +class Test_elb_is_in_multiple_az: + def test_elb_no_balancers(self): + from prowler.providers.aws.services.elb.elb_service import ELB + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider([AWS_REGION_EU_WEST_1]), + ), mock.patch( + "prowler.providers.aws.services.elb.elb_is_in_multiple_az.elb_is_in_multiple_az.elb_client", + new=ELB(set_mocked_aws_provider([AWS_REGION_EU_WEST_1])), + ): + # Test Check + from prowler.providers.aws.services.elb.elb_is_in_multiple_az.elb_is_in_multiple_az import ( + elb_is_in_multiple_az, + ) + + check = elb_is_in_multiple_az() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_elbv2_in_one_avaibility_zone(self): + elb_client = client("elb", region_name=AWS_REGION_EU_WEST_1) + # Create a compliant resource + + elb_client.create_load_balancer( + LoadBalancerName="my-lb", + Listeners=[ + {"Protocol": "tcp", "LoadBalancerPort": 80, "InstancePort": 8080}, + {"Protocol": "http", "LoadBalancerPort": 81, "InstancePort": 9000}, + ], + AvailabilityZones=[AWS_REGION_EU_WEST_1_AZA], + Scheme="internet-facing", + ) + + from prowler.providers.aws.services.elb.elb_service import ELB + + 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.elb.elb_is_in_multiple_az.elb_is_in_multiple_az.elb_client", + new=ELB(aws_provider), + ): + from prowler.providers.aws.services.elb.elb_is_in_multiple_az.elb_is_in_multiple_az import ( + elb_is_in_multiple_az, + ) + + check = elb_is_in_multiple_az() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Classic Load Balancer my-lb is not in at least 2 availability zones, it is only in {AWS_REGION_EU_WEST_1_AZA}." + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_id == "my-lb" + assert ( + result[0].resource_arn + == f"arn:aws:elasticloadbalancing:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:loadbalancer/my-lb" + ) + assert result[0].resource_tags == [] + + @mock_aws + def test_elbv2_in_multiple_avaibility_zones(self): + elb_client = client("elb", region_name=AWS_REGION_EU_WEST_1) + # Create a compliant resource + + elb_client.create_load_balancer( + LoadBalancerName="my-lb", + Listeners=[ + {"Protocol": "tcp", "LoadBalancerPort": 80, "InstancePort": 8080}, + {"Protocol": "http", "LoadBalancerPort": 81, "InstancePort": 9000}, + ], + AvailabilityZones=[AWS_REGION_EU_WEST_1_AZA, AWS_REGION_EU_WEST_1_AZB], + Scheme="internet-facing", + ) + + from prowler.providers.aws.services.elb.elb_service import ELB + + 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.elb.elb_is_in_multiple_az.elb_is_in_multiple_az.elb_client", + new=ELB(aws_provider), + ): + from prowler.providers.aws.services.elb.elb_is_in_multiple_az.elb_is_in_multiple_az import ( + elb_is_in_multiple_az, + ) + + check = elb_is_in_multiple_az() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Classic Load Balancer my-lb is in 2 availability zones: {AWS_REGION_EU_WEST_1_AZA}, {AWS_REGION_EU_WEST_1_AZB}." + ) or ( + result[0].status_extended + == f"Classic Load Balancer my-lb is in 2 availability zones: {AWS_REGION_EU_WEST_1_AZB}, {AWS_REGION_EU_WEST_1_AZA}." + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_id == "my-lb" + assert ( + result[0].resource_arn + == f"arn:aws:elasticloadbalancing:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:loadbalancer/my-lb" + ) + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/elb/elb_service_test.py b/tests/providers/aws/services/elb/elb_service_test.py index db835aec89..b4eb77a418 100644 --- a/tests/providers/aws/services/elb/elb_service_test.py +++ b/tests/providers/aws/services/elb/elb_service_test.py @@ -5,6 +5,7 @@ from prowler.providers.aws.services.elb.elb_service import ELB from tests.providers.aws.utils import ( AWS_ACCOUNT_NUMBER, AWS_REGION_US_EAST_1, + AWS_REGION_US_EAST_1_AZA, set_mocked_aws_provider, ) @@ -51,7 +52,7 @@ class Test_ELB_Service: {"Protocol": "tcp", "LoadBalancerPort": 80, "InstancePort": 8080}, {"Protocol": "http", "LoadBalancerPort": 81, "InstancePort": 9000}, ], - AvailabilityZones=[f"{AWS_REGION_US_EAST_1}a"], + AvailabilityZones=[AWS_REGION_US_EAST_1_AZA], Scheme="internal", SecurityGroups=[security_group.id], )["DNSName"] @@ -69,6 +70,8 @@ class Test_ELB_Service: assert elb.loadbalancers[elb_arn].listeners[0].policies == [] assert elb.loadbalancers[elb_arn].listeners[1].protocol == "HTTP" assert elb.loadbalancers[elb_arn].listeners[1].policies == [] + assert len(elb.loadbalancers[elb_arn].availability_zones) == 1 + assert AWS_REGION_US_EAST_1_AZA in elb.loadbalancers[elb_arn].availability_zones # Test ELB Describe Load Balancers Attributes @mock_aws