feat(ec2): add new check ec2_launch_template_imdsv2_required (#6139)

Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
This commit is contained in:
Daniel Barranquero
2024-12-12 16:27:20 +01:00
committed by GitHub
parent a90c772827
commit be347b2428
6 changed files with 283 additions and 1 deletions
@@ -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 <template-id> --version <version-number> --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": ""
}
@@ -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
@@ -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):
@@ -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 == []
@@ -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)