chore(AWS): allow ingress to any port for user defined network interface types (#4094)

Co-authored-by: Sergio <sergio@prowler.com>
This commit is contained in:
Kay Agahd
2024-05-31 18:37:52 +01:00
committed by GitHub
parent b2b66bd080
commit e45a189422
8 changed files with 435 additions and 48 deletions
+14
View File
@@ -40,6 +40,8 @@ The following list includes all the AWS checks with configurable variables that
| `cloudtrail_threat_detection_enumeration` | `threat_detection_enumeration_minutes` | Integer |
| `cloudtrail_threat_detection_enumeration` | `threat_detection_enumeration_actions` | List of Strings |
| `rds_instance_backup_enabled` | `check_rds_instance_replicas` | Boolean |
| `ec2_securitygroup_allow_ingress_from_internet_to_any_port` | `ec2_allowed_interface_types` | List of Strings |
| `ec2_securitygroup_allow_ingress_from_internet_to_any_port` | `ec2_allowed_instance_owners` | List of Strings |
## Azure
### Configurable Checks
@@ -96,6 +98,18 @@ aws:
max_security_group_rules: 50
# aws.ec2_instance_older_than_specific_days --> by default is 6 months (180 days)
max_ec2_instance_age_in_days: 180
# aws.ec2_securitygroup_allow_ingress_from_internet_to_any_port
# allowed network interface types for security groups open to the Internet
ec2_allowed_interface_types:
[
"api_gateway_managed",
"vpc_endpoint",
]
# allowed network interface owners for security groups open to the Internet
ec2_allowed_instance_owners:
[
"amazon-elb"
]
# AWS VPC Configuration (vpc_endpoint_connections_trust_boundaries, vpc_endpoint_services_allowed_principals_trust_boundaries)
# Single account environment: No action required. The AWS account number will be automatically added by the checks.
+12
View File
@@ -29,6 +29,18 @@ aws:
max_security_group_rules: 50
# aws.ec2_instance_older_than_specific_days --> by default is 6 months (180 days)
max_ec2_instance_age_in_days: 180
# aws.ec2_securitygroup_allow_ingress_from_internet_to_any_port
# allowed network interface types for security groups open to the Internet
ec2_allowed_interface_types:
[
"api_gateway_managed",
"vpc_endpoint",
]
# allowed network interface owners for security groups open to the Internet
ec2_allowed_instance_owners:
[
"amazon-elb"
]
# AWS VPC Configuration (vpc_endpoint_connections_trust_boundaries, vpc_endpoint_services_allowed_principals_trust_boundaries)
# Single account environment: No action required. The AWS account number will be automatically added by the checks.
@@ -1,5 +1,6 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.ec2.ec2_client import ec2_client
from prowler.providers.aws.services.ec2.ec2_service import NetworkInterface
from prowler.providers.aws.services.ec2.lib.security_groups import check_security_group
from prowler.providers.aws.services.vpc.vpc_client import vpc_client
from prowler.providers.aws.services.ec2.ec2_securitygroup_allow_ingress_from_internet_to_all_ports import (
@@ -35,11 +36,68 @@ class ec2_securitygroup_allow_ingress_from_internet_to_any_port(Check):
if check_security_group(
ingress_rule, "-1", ports=None, any_address=True
):
report.status = "FAIL"
report.status_extended = f"Security group {security_group.name} ({security_group.id}) has at least one port open to the Internet."
self.check_enis(
report=report,
security_group_name=security_group.name,
security_group_id=security_group.id,
enis=security_group.network_interfaces,
)
if report.status == "FAIL":
break # no need to check other ingress rules because at least one failed already
else:
report.status_extended = f"Security group {security_group.name} ({security_group.id}) has all ports open to the Internet and therefore was not checked against a specific port."
findings.append(report)
return findings
def check_enis(
self,
report,
security_group_name: str,
security_group_id: str,
enis: [NetworkInterface],
):
report.status_extended = f"Security group {security_group_name} ({security_group_id}) has at least one port open to the Internet but is exclusively not attached to any network interface."
for eni in enis:
if self.is_allowed_eni_type(eni_type=eni.type):
report.status = "PASS"
report.status_extended = f"Security group {security_group_name} ({security_group_id}) has at least one port open to the Internet but is exclusively attached to an allowed network interface type ({eni.type})."
continue
eni_owner = self.get_eni_owner(eni=eni)
if self.is_allowed_eni_owner(eni_owner=eni_owner):
report.status = "PASS"
report.status_extended = f"Security group {security_group_name} ({security_group_id}) has at least one port open to the Internet but is exclusively attached to an allowed network interface instance owner ({eni_owner})."
continue
else:
report.status = "FAIL"
report.status_extended = f"Security group {security_group_name} ({security_group_id}) has at least one port open to the Internet and neither its network interface type ({eni.type}) nor its network interface instance owner ({eni_owner}) are part of the allowed network interfaces."
break # no need to check other network interfaces because at least one failed already
@staticmethod
def is_allowed_eni_type(eni_type: str) -> bool:
return eni_type in ec2_client.audit_config.get(
"ec2_allowed_interface_types", []
)
@staticmethod
def get_eni_owner(eni) -> str:
eni_owner = ""
if (
hasattr(eni, "attachment")
and isinstance(eni.attachment, dict)
and "InstanceOwnerId" in eni.attachment
):
eni_owner = eni.attachment["InstanceOwnerId"]
return eni_owner
@staticmethod
def is_allowed_eni_owner(eni_owner: str) -> bool:
return eni_owner in ec2_client.audit_config.get(
"ec2_allowed_instance_owners", []
)
@@ -115,7 +115,6 @@ class EC2(AWSService):
is_resource_filtered(arn, self.audit_resources)
):
associated_sgs = []
# check if sg has public access to all ports
for ingress_rule in sg["IpPermissions"]:
# check associated security groups
for sg_group in ingress_rule.get("UserIdGroupPairs", []):
+5
View File
@@ -59,6 +59,11 @@ config_aws = {
"organizations_enabled_regions": [],
"organizations_trusted_delegated_administrators": [],
"check_rds_instance_replicas": False,
"ec2_allowed_interface_types": [
"api_gateway_managed",
"vpc_endpoint",
],
"ec2_allowed_instance_owners": ["amazon-elb"],
}
config_azure = {"shodan_api_key": None}
+13
View File
@@ -9,6 +9,19 @@ aws:
max_security_group_rules: 50
# aws.ec2_instance_older_than_specific_days --> by default is 6 months (180 days)
max_ec2_instance_age_in_days: 180
# aws.ec2_securitygroup_allow_ingress_from_internet_to_any_port
# allowed network interface types for security groups open to the Internet
ec2_allowed_interface_types:
[
"api_gateway_managed",
"vpc_endpoint",
]
# allowed network interface owners for security groups open to the Internet
ec2_allowed_instance_owners:
[
"amazon-elb"
]
# AWS VPC Configuration (vpc_endpoint_connections_trust_boundaries, vpc_endpoint_services_allowed_principals_trust_boundaries)
# Single account environment: No action required. The AWS account number will be automatically added by the checks.
+12
View File
@@ -5,6 +5,18 @@ shodan_api_key: null
max_security_group_rules: 50
# aws.ec2_instance_older_than_specific_days --> by default is 6 months (180 days)
max_ec2_instance_age_in_days: 180
# aws.ec2_securitygroup_allow_ingress_from_internet_to_any_port
# allowed network interface types for security groups open to the Internet
ec2_allowed_interface_types:
[
"api_gateway_managed",
"vpc_endpoint",
]
# allowed network interface owners for security groups open to the Internet
ec2_allowed_instance_owners:
[
"amazon-elb"
]
# AWS VPC Configuration (vpc_endpoint_connections_trust_boundaries, vpc_endpoint_services_allowed_principals_trust_boundaries)
# Single account environment: No action required. The AWS account number will be automatically added by the checks.
@@ -1,8 +1,10 @@
from unittest import mock
from boto3 import client, resource
from moto import mock_aws
from prowler.providers.aws.services.vpc.vpc_service import VPC
from tests.providers.aws.utils import (
AWS_REGION_EU_WEST_1,
@@ -53,12 +55,23 @@ class Test_ec2_securitygroup_allow_ingress_from_internet_to_any_port:
def test_ec2_non_compliant_default_sg(self):
# Create EC2 Mocked Resources
ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1)
ec2_client.create_vpc(CidrBlock="10.0.0.0/16")
vpc_response = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")
vpc_id = vpc_response["Vpc"]["VpcId"]
# Create Subnet
subnet_response = ec2_client.create_subnet(
VpcId=vpc_id, CidrBlock="10.0.1.0/24"
)
subnet_id = subnet_response["Subnet"]["SubnetId"]
default_sg = ec2_client.describe_security_groups(GroupNames=["default"])[
"SecurityGroups"
][0]
default_sg_id = default_sg["GroupId"]
default_sg_name = default_sg["GroupName"]
# Authorize ingress rule
ec2_client.authorize_security_group_ingress(
GroupId=default_sg_id,
IpPermissions=[
@@ -69,12 +82,31 @@ class Test_ec2_securitygroup_allow_ingress_from_internet_to_any_port:
],
)
# Create Network Interface
network_interface_response = ec2_client.create_network_interface(
SubnetId=subnet_id,
Groups=[
default_sg_id
], # Associating the network interface with the default security group
Description="Test Network Interface",
)
self.verify_check_fail(
default_sg_id, default_sg_name, network_interface_response
)
def verify_check_fail(
self, default_sg_id, default_sg_name, network_interface_response
):
eni = network_interface_response.get("NetworkInterface", {})
att = eni.get("Attachment", {})
eni_type = eni.get("InterfaceType", "")
eni_owner = att.get("InstanceOwnerId", "")
from prowler.providers.aws.services.ec2.ec2_service import EC2
aws_provider = set_mocked_aws_provider(
[AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1],
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
@@ -102,7 +134,266 @@ class Test_ec2_securitygroup_allow_ingress_from_internet_to_any_port:
assert sg.region == AWS_REGION_US_EAST_1
assert (
sg.status_extended
== f"Security group {default_sg_name} ({default_sg_id}) has at least one port open to the Internet."
== f"Security group {default_sg_name} ({default_sg_id}) has at least one port open to the Internet and neither its network interface type ({eni_type}) nor its network interface instance owner ({eni_owner}) are part of the allowed network interfaces."
)
assert (
sg.resource_arn
== f"arn:{aws_provider.identity.partition}:ec2:{AWS_REGION_US_EAST_1}:{aws_provider.identity.account}:security-group/{default_sg_id}"
)
assert sg.resource_details == default_sg_name
assert sg.resource_tags == []
@mock_aws
def test_check_enis(self):
aws_provider = set_mocked_aws_provider(
[AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1],
audit_config={
"ec2_allowed_interface_types": ["api_gateway_managed", "vpc_endpoint"],
"ec2_allowed_instance_owners": ["amazon-elb"],
},
)
from prowler.providers.aws.services.ec2.ec2_service import EC2
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), mock.patch(
"prowler.providers.aws.services.ec2.ec2_securitygroup_allow_ingress_from_internet_to_any_port.ec2_securitygroup_allow_ingress_from_internet_to_any_port.ec2_client",
new=EC2(aws_provider),
):
from prowler.providers.aws.services.ec2.ec2_securitygroup_allow_ingress_from_internet_to_any_port.ec2_securitygroup_allow_ingress_from_internet_to_any_port import (
ec2_securitygroup_allow_ingress_from_internet_to_any_port,
)
from unittest.mock import Mock
from prowler.providers.aws.services.ec2.ec2_service import NetworkInterface
tests = [
{
"eni_interface_type": "vpc_endpoint",
"eni_instance_owner": "NOT_ALLOWED",
"report": {
"status": "PASS",
"status_extended": "Security group SG_name (SG_id) has at least one port open to the Internet but is exclusively attached to an allowed network interface type (vpc_endpoint).",
},
},
{
"eni_interface_type": "NOT_ALLOWED",
"eni_instance_owner": "amazon-elb",
"report": {
"status": "PASS",
"status_extended": "Security group SG_name (SG_id) has at least one port open to the Internet but is exclusively attached to an allowed network interface instance owner (amazon-elb).",
},
},
{
"eni_interface_type": "NOT_ALLOWED_ENI_TYPE",
"eni_instance_owner": "NOT_ALLOWED_INSTANCE_OWNER",
"report": {
"status": "FAIL",
"status_extended": "Security group SG_name (SG_id) has at least one port open to the Internet and neither its network interface type (NOT_ALLOWED_ENI_TYPE) nor its network interface instance owner (NOT_ALLOWED_INSTANCE_OWNER) are part of the allowed network interfaces.",
},
},
]
check = ec2_securitygroup_allow_ingress_from_internet_to_any_port()
for test in tests:
eni = NetworkInterface(
id="1",
association={},
attachment={"InstanceOwnerId": test["eni_instance_owner"]},
private_ip="1",
type=test["eni_interface_type"],
subnet_id="1",
vpc_id="1",
region="1",
)
report = Mock()
check.check_enis(
report=report,
security_group_name="SG_name",
security_group_id="SG_id",
enis=[eni],
)
assert report.status == test["report"]["status"]
assert report.status_extended == test["report"]["status_extended"]
@mock_aws
def test_ec2_open_sg_attached_to_allowed_eni_type(self):
# Create EC2 Mocked Resources
ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1)
# Create VPC
vpc_response = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")
vpc_id = vpc_response["Vpc"]["VpcId"]
# Create Subnet
subnet_response = ec2_client.create_subnet(
VpcId=vpc_id, CidrBlock="10.0.1.0/24"
)
subnet_id = subnet_response["Subnet"]["SubnetId"]
# Get default security group
default_sg = ec2_client.describe_security_groups(
Filters=[
{"Name": "vpc-id", "Values": [vpc_id]},
{"Name": "group-name", "Values": ["default"]},
]
)["SecurityGroups"][0]
default_sg_id = default_sg["GroupId"]
default_sg_name = default_sg["GroupName"]
# Authorize ingress rule
ec2_client.authorize_security_group_ingress(
GroupId=default_sg_id,
IpPermissions=[
{
"IpProtocol": "-1",
"IpRanges": [{"CidrIp": "0.0.0.0/0"}],
}
],
)
# Create Network Interface
network_interface_response = ec2_client.create_network_interface(
SubnetId=subnet_id,
Groups=[
default_sg_id
], # Associating the network interface with the default security group
Description="Test Network Interface",
)
eni_type = network_interface_response["NetworkInterface"]["InterfaceType"]
from prowler.providers.aws.services.ec2.ec2_service import EC2
aws_provider = set_mocked_aws_provider(
[AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1],
audit_config={"ec2_allowed_interface_types": [eni_type]},
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), mock.patch(
"prowler.providers.aws.services.ec2.ec2_securitygroup_allow_ingress_from_internet_to_any_port.ec2_securitygroup_allow_ingress_from_internet_to_any_port.ec2_client",
new=EC2(aws_provider),
), mock.patch(
"prowler.providers.aws.services.ec2.ec2_securitygroup_allow_ingress_from_internet_to_any_port.ec2_securitygroup_allow_ingress_from_internet_to_any_port.vpc_client",
new=VPC(aws_provider),
):
# Test Check
from prowler.providers.aws.services.ec2.ec2_securitygroup_allow_ingress_from_internet_to_any_port.ec2_securitygroup_allow_ingress_from_internet_to_any_port import (
ec2_securitygroup_allow_ingress_from_internet_to_any_port,
)
check = ec2_securitygroup_allow_ingress_from_internet_to_any_port()
result = check.execute()
# One default sg per region
assert len(result) == 3
# Search changed sg
for sg in result:
if sg.resource_id == default_sg_id:
assert sg.status == "PASS"
assert sg.region == AWS_REGION_US_EAST_1
assert (
sg.status_extended
== f"Security group {default_sg_name} ({default_sg_id}) has at least one port open to the Internet but is exclusively attached to an allowed network interface type ({eni_type})."
)
assert (
sg.resource_arn
== f"arn:{aws_provider.identity.partition}:ec2:{AWS_REGION_US_EAST_1}:{aws_provider.identity.account}:security-group/{default_sg_id}"
)
assert sg.resource_details == default_sg_name
assert sg.resource_tags == []
@mock_aws
def test_ec2_open_sg_attached_to_allowed_eni_owner(self):
# Create EC2 Mocked Resources
ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1)
# Create VPC
vpc_response = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")
vpc_id = vpc_response["Vpc"]["VpcId"]
# Create Subnet
subnet_response = ec2_client.create_subnet(
VpcId=vpc_id, CidrBlock="10.0.1.0/24"
)
subnet_id = subnet_response["Subnet"]["SubnetId"]
# Get default security group
default_sg = ec2_client.describe_security_groups(
Filters=[
{"Name": "vpc-id", "Values": [vpc_id]},
{"Name": "group-name", "Values": ["default"]},
]
)["SecurityGroups"][0]
default_sg_id = default_sg["GroupId"]
default_sg_name = default_sg["GroupName"]
# Authorize ingress rule
ec2_client.authorize_security_group_ingress(
GroupId=default_sg_id,
IpPermissions=[
{
"IpProtocol": "-1",
"IpRanges": [{"CidrIp": "0.0.0.0/0"}],
}
],
)
# Create Network Interface
network_interface_response = ec2_client.create_network_interface(
SubnetId=subnet_id,
Groups=[
default_sg_id
], # Associating the network interface with the default security group
Description="Test Network Interface",
)
eni = network_interface_response.get("NetworkInterface", {})
att = eni.get("Attachment", {})
eni_owner = att.get("InstanceOwnerId", "")
from prowler.providers.aws.services.ec2.ec2_service import EC2
aws_provider = set_mocked_aws_provider(
[AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1],
audit_config={"ec2_allowed_instance_owners": [eni_owner]},
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), mock.patch(
"prowler.providers.aws.services.ec2.ec2_securitygroup_allow_ingress_from_internet_to_any_port.ec2_securitygroup_allow_ingress_from_internet_to_any_port.ec2_client",
new=EC2(aws_provider),
), mock.patch(
"prowler.providers.aws.services.ec2.ec2_securitygroup_allow_ingress_from_internet_to_any_port.ec2_securitygroup_allow_ingress_from_internet_to_any_port.vpc_client",
new=VPC(aws_provider),
):
# Test Check
from prowler.providers.aws.services.ec2.ec2_securitygroup_allow_ingress_from_internet_to_any_port.ec2_securitygroup_allow_ingress_from_internet_to_any_port import (
ec2_securitygroup_allow_ingress_from_internet_to_any_port,
)
check = ec2_securitygroup_allow_ingress_from_internet_to_any_port()
result = check.execute()
# One default sg per region
assert len(result) == 3
# Search changed sg
for sg in result:
if sg.resource_id == default_sg_id:
assert sg.status == "PASS"
assert sg.region == AWS_REGION_US_EAST_1
assert (
sg.status_extended
== f"Security group {default_sg_name} ({default_sg_id}) has at least one port open to the Internet but is exclusively attached to an allowed network interface instance owner ({eni_owner})."
)
assert (
sg.resource_arn
@@ -174,15 +465,27 @@ class Test_ec2_securitygroup_allow_ingress_from_internet_to_any_port:
assert sg.resource_tags == []
@mock_aws
def test_ec2_compliant_default_sg_only_open_to_one_port(self):
def test_ec2_non_compliant_default_sg_open_to_one_port(self):
# Create EC2 Mocked Resources
ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1)
ec2_client.create_vpc(CidrBlock="10.0.0.0/16")
# Create VPC
vpc_response = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")
vpc_id = vpc_response["Vpc"]["VpcId"]
# Create Subnet
subnet_response = ec2_client.create_subnet(
VpcId=vpc_id, CidrBlock="10.0.1.0/24"
)
subnet_id = subnet_response["Subnet"]["SubnetId"]
default_sg = ec2_client.describe_security_groups(GroupNames=["default"])[
"SecurityGroups"
][0]
default_sg_id = default_sg["GroupId"]
default_sg_name = default_sg["GroupName"]
# Authorize ingress rule
ec2_client.authorize_security_group_ingress(
GroupId=default_sg_id,
IpPermissions=[
@@ -198,47 +501,18 @@ class Test_ec2_securitygroup_allow_ingress_from_internet_to_any_port:
],
)
from prowler.providers.aws.services.ec2.ec2_service import EC2
aws_provider = set_mocked_aws_provider(
[AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1],
# Create Network Interface
network_interface_response = ec2_client.create_network_interface(
SubnetId=subnet_id,
Groups=[
default_sg_id
], # Associating the network interface with the default security group
Description="Test Network Interface",
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), mock.patch(
"prowler.providers.aws.services.ec2.ec2_securitygroup_allow_ingress_from_internet_to_any_port.ec2_securitygroup_allow_ingress_from_internet_to_any_port.ec2_client",
new=EC2(aws_provider),
), mock.patch(
"prowler.providers.aws.services.ec2.ec2_securitygroup_allow_ingress_from_internet_to_any_port.ec2_securitygroup_allow_ingress_from_internet_to_any_port.vpc_client",
new=VPC(aws_provider),
):
# Test Check
from prowler.providers.aws.services.ec2.ec2_securitygroup_allow_ingress_from_internet_to_any_port.ec2_securitygroup_allow_ingress_from_internet_to_any_port import (
ec2_securitygroup_allow_ingress_from_internet_to_any_port,
)
check = ec2_securitygroup_allow_ingress_from_internet_to_any_port()
result = check.execute()
# One default sg per region
assert len(result) == 3
# Search changed sg
for sg in result:
if sg.resource_id == default_sg_id:
assert sg.status == "FAIL"
assert sg.region == AWS_REGION_US_EAST_1
assert (
sg.status_extended
== f"Security group {default_sg_name} ({default_sg_id}) has at least one port open to the Internet."
)
assert (
sg.resource_arn
== f"arn:{aws_provider.identity.partition}:ec2:{AWS_REGION_US_EAST_1}:{aws_provider.identity.account}:security-group/{default_sg_id}"
)
assert sg.resource_details == default_sg_name
assert sg.resource_tags == []
self.verify_check_fail(
default_sg_id, default_sg_name, network_interface_response
)
@mock_aws
def test_ec2_default_sgs_ignoring(self):
@@ -316,7 +590,7 @@ class Test_ec2_securitygroup_allow_ingress_from_internet_to_any_port:
assert result[0].region == AWS_REGION_US_EAST_1
@mock_aws
def test_ec2_default_sgs_with_all_ports_check(self):
def test_ec2_default_sgs_with_any_ports_check(self):
# Create EC2 Mocked Resources
ec2 = resource("ec2", region_name=AWS_REGION_US_EAST_1)
vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")