diff --git a/prowler/providers/aws/services/elb/elb_connection_draining_enabled/__init__.py b/prowler/providers/aws/services/elb/elb_connection_draining_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/elb/elb_connection_draining_enabled/elb_connection_draining_enabled.metadata.json b/prowler/providers/aws/services/elb/elb_connection_draining_enabled/elb_connection_draining_enabled.metadata.json new file mode 100644 index 0000000000..1876ecc34c --- /dev/null +++ b/prowler/providers/aws/services/elb/elb_connection_draining_enabled/elb_connection_draining_enabled.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "elb_connection_draining_enabled", + "CheckTitle": "Classic Load Balancer Connection Draining Enabled", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], + "ServiceName": "elb", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "Severity": "medium", + "ResourceType": "AwsElbLoadBalancer", + "Description": "Checks if connection draining is enabled for Classic Load Balancers. Connection draining ensures that the load balancer stops sending requests to instances that are de-registering or unhealthy, while keeping existing connections open. This is particularly useful for instances in Auto Scaling groups, to ensure that connections aren't severed abruptly.", + "Risk": "Disabling connection draining can lead to abrupt connection termination for users, impacting the user experience and potentially causing application errors.", + "RelatedUrl": "https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/config-conn-drain.html", + "Remediation": { + "Code": { + "CLI": "aws elb modify-load-balancer-attributes --load-balancer-name --load-balancer-attributes '{'ConnectionDraining':{'Enabled':true,'Timeout':300}}'", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/elb-controls.html#elb-7", + "Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/ELB/elb-connection-draining-enabled.html#" + }, + "Recommendation": { + "Text": "Enable connection draining for all Classic Load Balancers. This ensures that existing connections are not abruptly terminated when instances are removed from the load balancer.", + "Url": "" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/elb/elb_connection_draining_enabled/elb_connection_draining_enabled.py b/prowler/providers/aws/services/elb/elb_connection_draining_enabled/elb_connection_draining_enabled.py new file mode 100644 index 0000000000..0c146fb516 --- /dev/null +++ b/prowler/providers/aws/services/elb/elb_connection_draining_enabled/elb_connection_draining_enabled.py @@ -0,0 +1,25 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.elb.elb_client import elb_client + + +class elb_connection_draining_enabled(Check): + def execute(self) -> list[Check_Report_AWS]: + findings = [] + 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 = "PASS" + report.status_extended = ( + f"ELB {load_balancer.name} has connection draining enabled." + ) + + if not load_balancer.connection_draining: + report.status = "FAIL" + report.status_extended = f"ELB {load_balancer.name} does not have connection draining enabled." + + 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 49b2494354..e2bfceffa8 100644 --- a/prowler/providers/aws/services/elb/elb_service.py +++ b/prowler/providers/aws/services/elb/elb_service.py @@ -64,6 +64,9 @@ class ELB(AWSService): load_balancer.cross_zone_load_balancing = attributes.get( "CrossZoneLoadBalancing", {} ).get("Enabled") + load_balancer.connection_draining = attributes.get( + "ConnectionDraining", {} + ).get("Enabled", False) except Exception as error: logger.error( @@ -101,4 +104,5 @@ class LoadBalancer(BaseModel): listeners: list[Listener] cross_zone_load_balancing: Optional[bool] availability_zones: set[str] + connection_draining: Optional[bool] tags: Optional[list] = [] diff --git a/tests/providers/aws/services/elb/elb_connection_draining_enabled/elb_connection_draining_enabled_test.py b/tests/providers/aws/services/elb/elb_connection_draining_enabled/elb_connection_draining_enabled_test.py new file mode 100644 index 0000000000..f77c434622 --- /dev/null +++ b/tests/providers/aws/services/elb/elb_connection_draining_enabled/elb_connection_draining_enabled_test.py @@ -0,0 +1,145 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) + + +class Test_elb_connection_draining_enabled: + + 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_connection_draining_enabled.elb_connection_draining_enabled.elb_client", + new=ELB(set_mocked_aws_provider([AWS_REGION_EU_WEST_1])), + ): + # Test Check + from prowler.providers.aws.services.elb.elb_connection_draining_enabled.elb_connection_draining_enabled import ( + elb_connection_draining_enabled, + ) + + check = elb_connection_draining_enabled() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_elb_draining_connection_enabled(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}, + ], + Scheme="internet-facing", + ) + + elb_client.modify_load_balancer_attributes( + LoadBalancerName="my-lb", + LoadBalancerAttributes={ + "ConnectionDraining": { + "Enabled": True, + "Timeout": 60, + } + }, + ) + + 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_connection_draining_enabled.elb_connection_draining_enabled.elb_client", + new=ELB(aws_provider), + ): + from prowler.providers.aws.services.elb.elb_connection_draining_enabled.elb_connection_draining_enabled import ( + elb_connection_draining_enabled, + ) + + check = elb_connection_draining_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "ELB my-lb has connection draining enabled." + ) + 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_elb_draining_connection_disabled(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}, + ], + Scheme="internet-facing", + ) + + elb_client.modify_load_balancer_attributes( + LoadBalancerName="my-lb", + LoadBalancerAttributes={ + "ConnectionDraining": { + "Enabled": False, + "Timeout": 60, + } + }, + ) + + 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_connection_draining_enabled.elb_connection_draining_enabled.elb_client", + new=ELB(aws_provider), + ): + from prowler.providers.aws.services.elb.elb_connection_draining_enabled.elb_connection_draining_enabled import ( + elb_connection_draining_enabled, + ) + + check = elb_connection_draining_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "ELB my-lb does not have connection draining enabled." + ) + 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 b4eb77a418..0222767e5e 100644 --- a/tests/providers/aws/services/elb/elb_service_test.py +++ b/tests/providers/aws/services/elb/elb_service_test.py @@ -104,6 +104,7 @@ class Test_ELB_Service: "S3BucketPrefix": "s3bf", }, "CrossZoneLoadBalancing": {"Enabled": True}, + "ConnectionDraining": {"Enabled": True, "Timeout": 60}, }, ) elb_arn = f"arn:aws:elasticloadbalancing:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:loadbalancer/my-lb" @@ -115,6 +116,7 @@ class Test_ELB_Service: assert elb.loadbalancers[elb_arn].scheme == "internal" assert elb.loadbalancers[elb_arn].access_logs assert elb.loadbalancers[elb_arn].cross_zone_load_balancing + assert elb.loadbalancers[elb_arn].connection_draining # Test ELB Describe Tags @mock_aws