feat(autoscaling): add new check autoscaling_group_multiple_instance_types (#5325)

Co-authored-by: Sergio <sergio@prowler.com>
This commit is contained in:
Daniel Barranquero
2024-10-15 21:36:48 +02:00
committed by GitHub
parent c0ac4c7c30
commit 45c32abcdf
6 changed files with 609 additions and 0 deletions
@@ -0,0 +1,32 @@
{
"Provider": "aws",
"CheckID": "autoscaling_group_multiple_instance_types",
"CheckTitle": "EC2 Auto Scaling Group should use multiple instance types in multiple Availability Zones.",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices"
],
"ServiceName": "autoscaling",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:autoscaling:region:account-id:autoScalingGroupName/resource-name",
"Severity": "medium",
"ResourceType": "AwsAutoScalingAutoScalingGroup",
"Description": "This control checks whether an Amazon EC2 Auto Scaling group uses multiple instance types in all the Availability Zones, meaning that there should be multiple Availability Zones with multiple instances on each one. The control fails if the Auto Scaling group has only one instance type defined.",
"Risk": "Using only one instance type in an Auto Scaling group reduces the flexibility to launch new instances when there is insufficient capacity for that specific type, potentially affecting the availability of the application.",
"RelatedUrl": "https://docs.aws.amazon.com/autoscaling/ec2/userguide/ec2-auto-scaling-mixed-instances-groups.html",
"Remediation": {
"Code": {
"CLI": "aws autoscaling create-auto-scaling-group --mixed-instances-policy ...",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/autoscaling-controls.html#autoscaling-6",
"Terraform": ""
},
"Recommendation": {
"Text": "Configure your EC2 Auto Scaling group to use multiple instance types across multiple Availability Zones.",
"Url": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/AutoScaling/asg-multiple-instance-type-az.html"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,34 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.autoscaling.autoscaling_client import (
autoscaling_client,
)
class autoscaling_group_multiple_instance_types(Check):
def execute(self):
findings = []
for group in autoscaling_client.groups:
report = Check_Report_AWS(self.metadata())
report.region = group.region
report.resource_id = group.name
report.resource_arn = group.arn
report.resource_tags = group.tags
report.status = "FAIL"
report.status_extended = f"Autoscaling group {group.name} does not have multiple instance types in multiple Availability Zones."
failing_azs = []
for az, types in group.az_instance_types.items():
if len(types) < 2:
failing_azs.append(az)
if not failing_azs and len(group.az_instance_types) > 1:
report.status = "PASS"
report.status_extended = f"Autoscaling group {group.name} has multiple instance types in each of its Availability Zones."
elif failing_azs:
azs_str = ", ".join(failing_azs)
report.status_extended = f"Autoscaling group {group.name} has only one or no instance types in Availability Zone(s): {azs_str}."
findings.append(report)
return findings
@@ -66,6 +66,16 @@ class AutoScaling(AWSService):
self.audit_resources,
)
):
instance_types = []
az_instance_types = {}
for instance in group.get("Instances", []):
az = instance["AvailabilityZone"]
instance_type = instance["InstanceType"]
instance_types.append(instance_type)
if az not in az_instance_types:
az_instance_types[az] = set()
az_instance_types[az].add(instance_type)
self.groups.append(
Group(
arn=group.get("AutoScalingGroupARN"),
@@ -73,6 +83,8 @@ class AutoScaling(AWSService):
region=regional_client.region,
availability_zones=group.get("AvailabilityZones"),
tags=group.get("Tags"),
instance_types=instance_types,
az_instance_types=az_instance_types,
launch_template=group.get("LaunchTemplate", {}),
mixed_instances_policy_launch_template=group.get(
"MixedInstancesPolicy", {}
@@ -154,6 +166,8 @@ class Group(BaseModel):
region: str
availability_zones: list
tags: list = []
instance_types: list = []
az_instance_types: dict = {}
launch_template: dict = {}
mixed_instances_policy_launch_template: dict = {}
health_check_type: str
@@ -0,0 +1,418 @@
from unittest import mock
import botocore
from boto3 import client
from moto import mock_aws
from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider
make_api_call = botocore.client.BaseClient._make_api_call
def mock_make_api_call_multi_az(self, operation_name, kwarg):
if operation_name == "DescribeAutoScalingGroups":
return {
"AutoScalingGroups": [
{
"AutoScalingGroupName": "my-autoscaling-group",
"AutoScalingGroupARN": "arn:aws:autoscaling:us-east-1:123456789012:autoScalingGroup:uuid:autoScalingGroupName/my-autoscaling-group",
"AvailabilityZones": ["us-east-1a", "us-east-1b"],
"Tags": [],
"Instances": [
{
"InstanceId": "i-0b9f1f3a0e1e3e0f4",
"InstanceType": "t2.micro",
"AvailabilityZone": "us-east-1a",
},
{
"InstanceId": "i-0b9f1f3a0e1e3e0f5",
"InstanceType": "t3.large",
"AvailabilityZone": "us-east-1b",
},
],
}
]
}
return make_api_call(self, operation_name, kwarg)
def mock_make_api_call_single_az(self, operation_name, kwarg):
if operation_name == "DescribeAutoScalingGroups":
return {
"AutoScalingGroups": [
{
"AutoScalingGroupName": "my-autoscaling-group",
"AutoScalingGroupARN": "arn:aws:autoscaling:us-east-1:123456789012:autoScalingGroup:uuid:autoScalingGroupName/my-autoscaling-group",
"AvailabilityZones": ["us-east-1a"],
"Tags": [],
"Instances": [
{
"InstanceId": "i-0b9f1f3a0e1e3e0f4",
"InstanceType": "t2.micro",
"AvailabilityZone": "us-east-1a",
},
{
"InstanceId": "i-0b9f1f3a0e1e3e0f5",
"InstanceType": "t3.large",
"AvailabilityZone": "us-east-1a",
},
],
}
]
}
return make_api_call(self, operation_name, kwarg)
def mock_make_api_call(self, operation_name, kwarg):
if operation_name == "DescribeAutoScalingGroups":
return {
"AutoScalingGroups": [
{
"AutoScalingGroupName": "my-autoscaling-group",
"AutoScalingGroupARN": "arn:aws:autoscaling:us-east-1:123456789012:autoScalingGroup:uuid:autoScalingGroupName/my-autoscaling-group",
"AvailabilityZones": ["us-east-1a", "us-east-1b"],
"Tags": [],
"Instances": [
{
"InstanceId": "i-0b9f1f3a0e1e3e0f4",
"InstanceType": "t2.micro",
"AvailabilityZone": "us-east-1a",
},
{
"InstanceId": "i-0b9f1f3a0e1e3e0f5",
"InstanceType": "t3.large",
"AvailabilityZone": "us-east-1a",
},
{
"InstanceId": "i-0b9f1f3a0e1e3e0f6",
"InstanceType": "t2.micro",
"AvailabilityZone": "us-east-1b",
},
{
"InstanceId": "i-0b9f1f3a0e1e3e0f7",
"InstanceType": "t3.large",
"AvailabilityZone": "us-east-1b",
},
],
}
]
}
return make_api_call(self, operation_name, kwarg)
class Test_autoscaling_group_multiple_instance_types:
@mock_aws
def test_no_autoscaling(self):
autoscaling_client = client("autoscaling", region_name=AWS_REGION_US_EAST_1)
autoscaling_client.groups = []
from prowler.providers.aws.services.autoscaling.autoscaling_service import (
AutoScaling,
)
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), mock.patch(
"prowler.providers.aws.services.autoscaling.autoscaling_group_multiple_instance_types.autoscaling_group_multiple_instance_types.autoscaling_client",
new=AutoScaling(aws_provider),
):
# Test Check
from prowler.providers.aws.services.autoscaling.autoscaling_group_multiple_instance_types.autoscaling_group_multiple_instance_types import (
autoscaling_group_multiple_instance_types,
)
check = autoscaling_group_multiple_instance_types()
result = check.execute()
assert len(result) == 0
@mock_aws
def test_groups_with_multi_az_one_or_less_instances(self):
autoscaling_client = client("autoscaling", region_name=AWS_REGION_US_EAST_1)
autoscaling_client.create_launch_configuration(
LaunchConfigurationName="test",
ImageId="ami-12c6146b",
InstanceType="t1.micro",
KeyName="the_keys",
SecurityGroups=["default", "default2"],
)
autoscaling_group_name = "my-autoscaling-group"
autoscaling_client.create_auto_scaling_group(
AutoScalingGroupName=autoscaling_group_name,
LaunchConfigurationName="test",
MinSize=0,
MaxSize=0,
DesiredCapacity=0,
AvailabilityZones=["us-east-1a", "us-east-1b"],
)
autoscaling_group_arn = autoscaling_client.describe_auto_scaling_groups(
AutoScalingGroupNames=[autoscaling_group_name]
)["AutoScalingGroups"][0]["AutoScalingGroupARN"]
from prowler.providers.aws.services.autoscaling.autoscaling_service import (
AutoScaling,
)
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), mock.patch(
"prowler.providers.aws.services.autoscaling.autoscaling_group_multiple_instance_types.autoscaling_group_multiple_instance_types.autoscaling_client",
new=AutoScaling(aws_provider),
):
# Test Check
from prowler.providers.aws.services.autoscaling.autoscaling_group_multiple_instance_types.autoscaling_group_multiple_instance_types import (
autoscaling_group_multiple_instance_types,
)
check = autoscaling_group_multiple_instance_types()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Autoscaling group {autoscaling_group_name} does not have multiple instance types in multiple Availability Zones."
)
assert result[0].resource_id == autoscaling_group_name
assert result[0].resource_arn == autoscaling_group_arn
assert result[0].region == AWS_REGION_US_EAST_1
assert result[0].resource_tags == []
@mock_aws
def test_groups_with_single_az_one_or_less_instances(self):
autoscaling_client = client("autoscaling", region_name=AWS_REGION_US_EAST_1)
autoscaling_client.create_launch_configuration(
LaunchConfigurationName="test",
ImageId="ami-12c6146b",
InstanceType="t1.micro",
KeyName="the_keys",
SecurityGroups=["default", "default2"],
)
autoscaling_group_name = "my-autoscaling-group"
autoscaling_client.create_auto_scaling_group(
AutoScalingGroupName=autoscaling_group_name,
LaunchConfigurationName="test",
MinSize=0,
MaxSize=0,
DesiredCapacity=0,
AvailabilityZones=["us-east-1a"],
)
autoscaling_group_arn = autoscaling_client.describe_auto_scaling_groups(
AutoScalingGroupNames=[autoscaling_group_name]
)["AutoScalingGroups"][0]["AutoScalingGroupARN"]
from prowler.providers.aws.services.autoscaling.autoscaling_service import (
AutoScaling,
)
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), mock.patch(
"prowler.providers.aws.services.autoscaling.autoscaling_group_multiple_instance_types.autoscaling_group_multiple_instance_types.autoscaling_client",
new=AutoScaling(aws_provider),
):
# Test Check
from prowler.providers.aws.services.autoscaling.autoscaling_group_multiple_instance_types.autoscaling_group_multiple_instance_types import (
autoscaling_group_multiple_instance_types,
)
check = autoscaling_group_multiple_instance_types()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Autoscaling group {autoscaling_group_name} does not have multiple instance types in multiple Availability Zones."
)
assert result[0].resource_id == autoscaling_group_name
assert result[0].resource_tags == []
assert result[0].resource_arn == autoscaling_group_arn
@mock_aws
def test_groups_with_multi_az_multi_instances_but_not_in_each_az(self):
with mock.patch(
"botocore.client.BaseClient._make_api_call", new=mock_make_api_call_multi_az
):
autoscaling_client = client("autoscaling", region_name=AWS_REGION_US_EAST_1)
autoscaling_client.create_launch_configuration(
LaunchConfigurationName="test",
ImageId="ami-12c6146b",
InstanceType="t1.micro",
KeyName="the_keys",
SecurityGroups=["default", "default2"],
)
autoscaling_group_name = "my-autoscaling-group"
autoscaling_client.create_auto_scaling_group(
AutoScalingGroupName=autoscaling_group_name,
LaunchConfigurationName="test",
MinSize=0,
MaxSize=0,
DesiredCapacity=0,
AvailabilityZones=["us-east-1a", "us-east-1b"],
)
autoscaling_group_arn = autoscaling_client.describe_auto_scaling_groups(
AutoScalingGroupNames=[autoscaling_group_name]
)["AutoScalingGroups"][0]["AutoScalingGroupARN"]
from prowler.providers.aws.services.autoscaling.autoscaling_service import (
AutoScaling,
)
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), mock.patch(
"prowler.providers.aws.services.autoscaling.autoscaling_group_multiple_instance_types.autoscaling_group_multiple_instance_types.autoscaling_client",
new=AutoScaling(aws_provider),
):
# Test Check
from prowler.providers.aws.services.autoscaling.autoscaling_group_multiple_instance_types.autoscaling_group_multiple_instance_types import (
autoscaling_group_multiple_instance_types,
)
check = autoscaling_group_multiple_instance_types()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Autoscaling group {autoscaling_group_name} has only one or no instance types in Availability Zone(s): us-east-1a, us-east-1b."
)
assert result[0].resource_id == autoscaling_group_name
assert result[0].resource_arn == autoscaling_group_arn
assert result[0].region == AWS_REGION_US_EAST_1
assert result[0].resource_tags == []
@mock_aws
def test_groups_with_single_az_multi_instances(self):
with mock.patch(
"botocore.client.BaseClient._make_api_call",
new=mock_make_api_call_single_az,
):
autoscaling_client = client("autoscaling", region_name=AWS_REGION_US_EAST_1)
autoscaling_client.create_launch_configuration(
LaunchConfigurationName="test",
ImageId="ami-12c6146b",
InstanceType="t1.micro",
KeyName="the_keys",
SecurityGroups=["default", "default2"],
)
autoscaling_group_name = "my-autoscaling-group"
autoscaling_client.create_auto_scaling_group(
AutoScalingGroupName=autoscaling_group_name,
LaunchConfigurationName="test",
MinSize=0,
MaxSize=0,
DesiredCapacity=0,
AvailabilityZones=["us-east-1a"],
)
autoscaling_group_arn = autoscaling_client.describe_auto_scaling_groups(
AutoScalingGroupNames=[autoscaling_group_name]
)["AutoScalingGroups"][0]["AutoScalingGroupARN"]
from prowler.providers.aws.services.autoscaling.autoscaling_service import (
AutoScaling,
)
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), mock.patch(
"prowler.providers.aws.services.autoscaling.autoscaling_group_multiple_instance_types.autoscaling_group_multiple_instance_types.autoscaling_client",
new=AutoScaling(aws_provider),
):
# Test Check
from prowler.providers.aws.services.autoscaling.autoscaling_group_multiple_instance_types.autoscaling_group_multiple_instance_types import (
autoscaling_group_multiple_instance_types,
)
check = autoscaling_group_multiple_instance_types()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Autoscaling group {autoscaling_group_name} does not have multiple instance types in multiple Availability Zones."
)
assert result[0].resource_id == autoscaling_group_name
assert result[0].resource_tags == []
assert result[0].resource_arn == autoscaling_group_arn
@mock_aws
def test_groups_with_multi_az_multi_instances_in_each_one(self):
with mock.patch(
"botocore.client.BaseClient._make_api_call", new=mock_make_api_call
):
autoscaling_client = client("autoscaling", region_name=AWS_REGION_US_EAST_1)
autoscaling_client.create_launch_configuration(
LaunchConfigurationName="test",
ImageId="ami-12c6146b",
InstanceType="t1.micro",
KeyName="the_keys",
SecurityGroups=["default", "default2"],
)
autoscaling_group_name = "my-autoscaling-group"
autoscaling_client.create_auto_scaling_group(
AutoScalingGroupName=autoscaling_group_name,
LaunchConfigurationName="test",
MinSize=0,
MaxSize=0,
DesiredCapacity=0,
AvailabilityZones=["us-east-1a", "us-east-1b"],
)
autoscaling_group_arn = autoscaling_client.describe_auto_scaling_groups(
AutoScalingGroupNames=[autoscaling_group_name]
)["AutoScalingGroups"][0]["AutoScalingGroupARN"]
from prowler.providers.aws.services.autoscaling.autoscaling_service import (
AutoScaling,
)
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), mock.patch(
"prowler.providers.aws.services.autoscaling.autoscaling_group_multiple_instance_types.autoscaling_group_multiple_instance_types.autoscaling_client",
new=AutoScaling(aws_provider),
):
# Test Check
from prowler.providers.aws.services.autoscaling.autoscaling_group_multiple_instance_types.autoscaling_group_multiple_instance_types import (
autoscaling_group_multiple_instance_types,
)
check = autoscaling_group_multiple_instance_types()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Autoscaling group {autoscaling_group_name} has multiple instance types in each of its Availability Zones."
)
assert result[0].resource_id == autoscaling_group_name
assert result[0].resource_arn == autoscaling_group_arn
assert result[0].region == AWS_REGION_US_EAST_1
assert result[0].resource_tags == []
@@ -1,5 +1,7 @@
from base64 import b64decode
import botocore
import mock
from boto3 import client
from moto import mock_aws
@@ -14,6 +16,53 @@ from tests.providers.aws.utils import (
set_mocked_aws_provider,
)
make_api_call = botocore.client.BaseClient._make_api_call
def mock_make_api_call(self, operation_name, kwarg):
if operation_name == "DescribeAutoScalingGroups":
return {
"AutoScalingGroups": [
{
"AutoScalingGroupName": "my-autoscaling-group",
"AutoScalingGroupARN": "arn:aws:autoscaling:us-east-1:123456789012:autoScalingGroup:uuid:autoScalingGroupName/my-autoscaling-group",
"AvailabilityZones": ["us-east-1a", "us-east-1b"],
"Tags": [
{
"Key": "tag_test",
"PropagateAtLaunch": False,
"ResourceId": "my-autoscaling-group",
"ResourceType": "auto-scaling-group",
"Value": "value_test",
}
],
"Instances": [
{
"InstanceId": "i-0b9f1f3a0e1e3e0f4",
"InstanceType": "t2.micro",
"AvailabilityZone": "us-east-1a",
},
{
"InstanceId": "i-0b9f1f3a0e1e3e0f5",
"InstanceType": "t3.large",
"AvailabilityZone": "us-east-1a",
},
{
"InstanceId": "i-0b9f1f3a0e1e3e0f6",
"InstanceType": "t2.micro",
"AvailabilityZone": "us-east-1b",
},
{
"InstanceId": "i-0b9f1f3a0e1e3e0f7",
"InstanceType": "t3.large",
"AvailabilityZone": "us-east-1b",
},
],
}
]
}
return make_api_call(self, operation_name, kwarg)
class Test_AutoScaling_Service:
# Test AutoScaling Service
@@ -105,6 +154,68 @@ class Test_AutoScaling_Service:
assert launch_configurations[arn_tester2].http_tokens == "required"
assert launch_configurations[arn_tester2].http_endpoint == "enabled"
# Test Describe Auto Scaling Groups With Botocore
@mock_aws
def test_describe_auto_scaling_groups_with_attached_instances(self):
with mock.patch(
"botocore.client.BaseClient._make_api_call", new=mock_make_api_call
):
# Generate AutoScaling Client
autoscaling_client = client("autoscaling", region_name=AWS_REGION_US_EAST_1)
autoscaling_client.create_launch_configuration(
LaunchConfigurationName="test",
ImageId="ami-12c6146b",
InstanceType="t1.micro",
KeyName="the_keys",
SecurityGroups=["default", "default2"],
)
autoscaling_client.create_auto_scaling_group(
AutoScalingGroupName="my-autoscaling-group",
LaunchConfigurationName="test",
MinSize=0,
MaxSize=0,
DesiredCapacity=0,
AvailabilityZones=["us-east-1a", "us-east-1b"],
Tags=[
{
"Key": "tag_test",
"Value": "value_test",
},
],
)
# AutoScaling client for this test class
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
autoscaling = AutoScaling(aws_provider)
assert len(autoscaling.groups) == 1
# create_auto_scaling_group doesn't return the ARN, can't check it
# assert autoscaling.groups[0].arn ==
assert autoscaling.groups[0].name == "my-autoscaling-group"
assert autoscaling.groups[0].region == AWS_REGION_US_EAST_1
assert autoscaling.groups[0].availability_zones == [
"us-east-1a",
"us-east-1b",
]
assert autoscaling.groups[0].tags == [
{
"Key": "tag_test",
"PropagateAtLaunch": False,
"ResourceId": "my-autoscaling-group",
"ResourceType": "auto-scaling-group",
"Value": "value_test",
}
]
assert autoscaling.groups[0].instance_types == [
"t2.micro",
"t3.large",
"t2.micro",
"t3.large",
]
assert autoscaling.groups[0].az_instance_types == {
"us-east-1a": {"t2.micro", "t3.large"},
"us-east-1b": {"t2.micro", "t3.large"},
}
# Test Describe Auto Scaling Groups
@mock_aws
def test_describe_auto_scaling_groups(self):