From be347b2428fd65fc90830ce65bef1d92a4c37120 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:27:20 +0100 Subject: [PATCH] feat(ec2): add new check `ec2_launch_template_imdsv2_required` (#6139) Co-authored-by: MrCloudSec --- .../__init__.py | 0 ...nch_template_imdsv2_required.metadata.json | 32 +++ .../ec2_launch_template_imdsv2_required.py | 42 ++++ .../providers/aws/services/ec2/ec2_service.py | 9 +- ...c2_launch_template_imdsv2_required_test.py | 195 ++++++++++++++++++ .../aws/services/ec2/ec2_service_test.py | 6 + 6 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/__init__.py create mode 100644 prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required.metadata.json create mode 100644 prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required.py create mode 100644 tests/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required_test.py diff --git a/prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/__init__.py b/prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required.metadata.json b/prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required.metadata.json new file mode 100644 index 0000000000..591225a7ce --- /dev/null +++ b/prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "ec2_launch_template_imdsv2_required", + "CheckTitle": "Amazon EC2 launch templates should have IMDSv2 enabled and required.", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "ec2", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:ec2:region:account-id:launch-template/resource-id", + "Severity": "high", + "ResourceType": "AwsEc2LaunchTemplate", + "Description": "This control checks if Amazon EC2 launch templates are configured with IMDSv2 enabled and required. The control fails if IMDSv2 is not enabled or required in the launch template versions.", + "Risk": "Without IMDSv2 required, EC2 instances may be vulnerable to metadata service attacks, allowing unauthorized access to instance metadata, potentially leading to compromise of instance credentials or other sensitive data.", + "RelatedUrl": "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html", + "Remediation": { + "Code": { + "CLI": "aws ec2 modify-launch-template --launch-template-id --version --metadata-options HttpTokens=required", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ec2-controls.html#ec2-170", + "Terraform": "" + }, + "Recommendation": { + "Text": "To ensure EC2 launch templates have IMDSv2 enabled and required, update the template to configure the Instance Metadata Service Version 2 as required.", + "Url": "https://docs.aws.amazon.com/autoscaling/ec2/userguide/create-launch-template.html#change-metadata-options" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required.py b/prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required.py new file mode 100644 index 0000000000..377d7519ed --- /dev/null +++ b/prowler/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required.py @@ -0,0 +1,42 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.ec2.ec2_client import ec2_client + + +class ec2_launch_template_imdsv2_required(Check): + def execute(self): + findings = [] + for template in ec2_client.launch_templates: + report = Check_Report_AWS(self.metadata()) + report.region = template.region + report.resource_id = template.id + report.resource_arn = template.arn + report.resource_tags = template.tags + + versions_with_imdsv2_required = [] + versions_with_metadata_disabled = [] + versions_with_no_imdsv2 = [] + + for version in template.versions: + if ( + version.template_data.http_endpoint == "enabled" + and version.template_data.http_tokens == "required" + ): + versions_with_imdsv2_required.append(str(version.version_number)) + elif version.template_data.http_endpoint == "disabled": + versions_with_metadata_disabled.append(str(version.version_number)) + else: + versions_with_no_imdsv2.append(str(version.version_number)) + + if versions_with_imdsv2_required: + report.status = "PASS" + report.status_extended = f"EC2 Launch Template {template.name} has IMDSv2 enabled and required in the following versions: {', '.join(versions_with_imdsv2_required)}." + elif versions_with_metadata_disabled: + report.status = "PASS" + report.status_extended = f"EC2 Launch Template {template.name} has metadata service disabled in the following versions: {', '.join(versions_with_metadata_disabled)}." + else: + report.status = "FAIL" + report.status_extended = f"EC2 Launch Template {template.name} has IMDSv2 disabled or not required in the following versions: {', '.join(versions_with_no_imdsv2)}." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/ec2/ec2_service.py b/prowler/providers/aws/services/ec2/ec2_service.py index 4c695c3110..cd559128f8 100644 --- a/prowler/providers/aws/services/ec2/ec2_service.py +++ b/prowler/providers/aws/services/ec2/ec2_service.py @@ -10,7 +10,6 @@ from prowler.lib.scan_filters.scan_filters import is_resource_filtered from prowler.providers.aws.lib.service.service import AWSService -################## EC2 class EC2(AWSService): def __init__(self, provider): # Call AWSService's __init__ @@ -569,6 +568,12 @@ class EC2(AWSService): ), network_interfaces=enis, associate_public_ip_address=associate_public_ip, + http_tokens=template_version["LaunchTemplateData"] + .get("MetadataOptions", {}) + .get("HttpTokens", ""), + http_endpoint=template_version["LaunchTemplateData"] + .get("MetadataOptions", {}) + .get("HttpEndpoint", ""), ), ) ) @@ -763,6 +768,8 @@ class TemplateData(BaseModel): user_data: str network_interfaces: Optional[list[NetworkInterface]] associate_public_ip_address: Optional[bool] + http_tokens: Optional[str] + http_endpoint: Optional[str] class LaunchTemplateVersion(BaseModel): diff --git a/tests/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required_test.py b/tests/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required_test.py new file mode 100644 index 0000000000..22ff04f349 --- /dev/null +++ b/tests/providers/aws/services/ec2/ec2_launch_template_imdsv2_required/ec2_launch_template_imdsv2_required_test.py @@ -0,0 +1,195 @@ +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(self, operation_name, kwarg): + if operation_name == "DescribeLaunchTemplateVersions": + return { + "LaunchTemplateVersions": [ + { + "VersionNumber": 1, + "LaunchTemplateData": { + "MetadataOptions": { + "HttpEndpoint": "enabled", + "HttpTokens": "required", + } + }, + } + ] + } + return make_api_call(self, operation_name, kwarg) + + +def mock_make_api_call_not_required(self, operation_name, kwarg): + if operation_name == "DescribeLaunchTemplateVersions": + return { + "LaunchTemplateVersions": [ + { + "VersionNumber": 1, + "LaunchTemplateData": { + "MetadataOptions": { + "HttpEndpoint": "enabled", + "HttpTokens": "optional", + } + }, + } + ] + } + return make_api_call(self, operation_name, kwarg) + + +class Test_ec2_launch_template_imdsv2_required: + @mock_aws + def test_no_launch_templates(self): + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + ec2_client.launch_templates = [] + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + 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.ec2.ec2_launch_template_imdsv2_required.ec2_launch_template_imdsv2_required.ec2_client", + new=EC2(aws_provider), + ), + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_launch_template_imdsv2_required.ec2_launch_template_imdsv2_required import ( + ec2_launch_template_imdsv2_required, + ) + + check = ec2_launch_template_imdsv2_required() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_launch_template_imdsv2_required(self): + with mock.patch( + "botocore.client.BaseClient._make_api_call", new=mock_make_api_call + ): + launch_template_name = "test-imdsv2-required" + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + ec2_client.create_launch_template( + LaunchTemplateName=launch_template_name, + VersionDescription="Launch Template with IMDSv2 required", + LaunchTemplateData={ + "InstanceType": "t1.micro", + "MetadataOptions": { + "HttpEndpoint": "enabled", + "HttpTokens": "required", + }, + }, + ) + + launch_template_id = ec2_client.describe_launch_templates( + LaunchTemplateNames=[launch_template_name] + )["LaunchTemplates"][0]["LaunchTemplateId"] + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + 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.ec2.ec2_launch_template_imdsv2_required.ec2_launch_template_imdsv2_required.ec2_client", + new=EC2(aws_provider), + ), + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_launch_template_imdsv2_required.ec2_launch_template_imdsv2_required import ( + ec2_launch_template_imdsv2_required, + ) + + check = ec2_launch_template_imdsv2_required() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"EC2 Launch Template {launch_template_name} has IMDSv2 enabled and required in the following versions: 1." + ) + assert result[0].resource_id == launch_template_id + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:123456789012:launch-template/{launch_template_id}" + ) + assert result[0].resource_tags == [] + + @mock_aws + def test_launch_template_imdsv2_not_required(self): + with mock.patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_not_required, + ): + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + launch_template_name = "test-imdsv2-not-required" + ec2_client.create_launch_template( + LaunchTemplateName=launch_template_name, + VersionDescription="Launch Template without IMDSv2 required", + LaunchTemplateData={ + "InstanceType": "t1.micro", + "MetadataOptions": { + "HttpEndpoint": "enabled", + "HttpTokens": "optional", + }, + }, + ) + + launch_template_id = ec2_client.describe_launch_templates( + LaunchTemplateNames=[launch_template_name] + )["LaunchTemplates"][0]["LaunchTemplateId"] + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + 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.ec2.ec2_launch_template_imdsv2_required.ec2_launch_template_imdsv2_required.ec2_client", + new=EC2(aws_provider), + ), + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_launch_template_imdsv2_required.ec2_launch_template_imdsv2_required import ( + ec2_launch_template_imdsv2_required, + ) + + check = ec2_launch_template_imdsv2_required() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"EC2 Launch Template {launch_template_name} has IMDSv2 disabled or not required in the following versions: 1." + ) + assert result[0].resource_id == launch_template_id + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:123456789012:launch-template/{launch_template_id}" + ) + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/ec2/ec2_service_test.py b/tests/providers/aws/services/ec2/ec2_service_test.py index f4fd93b14a..629e319f38 100644 --- a/tests/providers/aws/services/ec2/ec2_service_test.py +++ b/tests/providers/aws/services/ec2/ec2_service_test.py @@ -36,6 +36,10 @@ def mock_make_api_call(self, operation_name, kwarg): "foobar123".encode(encoding_format_utf_8) ).decode(encoding_format_utf_8), "NetworkInterfaces": [{"AssociatePublicIpAddress": True}], + "MetadataOptions": { + "HttpEndpoint": "enabled", + "HttpTokens": "optional", + }, }, } ] @@ -741,6 +745,8 @@ class Test_EC2_Service: ) assert version.template_data.associate_public_ip_address + assert version.template_data.http_endpoint == "enabled" + assert version.template_data.http_tokens == "optional" # Test EC2 Describe VPN Endpoints @mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call)