feat(elb): add new check elb_is_in_multiple_az (#4829)

Co-authored-by: Sergio Garcia <38561120+sergargar@users.noreply.github.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
This commit is contained in:
Rubén De la Torre Vico
2024-08-26 13:27:08 +02:00
committed by GitHub
parent 71f50422ad
commit bcb63d0b2d
10 changed files with 205 additions and 1 deletions
+1
View File
@@ -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
+5
View File
@@ -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
@@ -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:<partition>:elasticloadbalancing:<region>:<account-id>:loadbalancer/<load-balancer-name>",
"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": ""
}
@@ -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
@@ -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] = []
+1
View File
@@ -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,
}
+5
View File
@@ -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
@@ -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 == []
@@ -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