diff --git a/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_any_port/ec2_networkacl_allow_ingress_any_port.py b/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_any_port/ec2_networkacl_allow_ingress_any_port.py index 444f73527c..abd5857e83 100644 --- a/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_any_port/ec2_networkacl_allow_ingress_any_port.py +++ b/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_any_port/ec2_networkacl_allow_ingress_any_port.py @@ -8,7 +8,7 @@ class ec2_networkacl_allow_ingress_any_port(Check): findings = [] tcp_protocol = "-1" check_port = 0 - for network_acl in ec2_client.network_acls: + for arn, network_acl in ec2_client.network_acls.items(): if ( ec2_client.provider.scan_unused_services or network_acl.region in ec2_client.regions_with_sgs @@ -18,7 +18,7 @@ class ec2_networkacl_allow_ingress_any_port(Check): report = Check_Report_AWS(self.metadata()) report.resource_id = network_acl.id report.region = network_acl.region - report.resource_arn = network_acl.arn + report.resource_arn = arn report.resource_tags = network_acl.tags report.status = "FAIL" report.status_extended = f"Network ACL {network_acl.name if network_acl.name else network_acl.id} has every port open to the Internet." diff --git a/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_tcp_port_22/ec2_networkacl_allow_ingress_tcp_port_22.py b/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_tcp_port_22/ec2_networkacl_allow_ingress_tcp_port_22.py index 13d580c3f5..1d3f01ef6e 100644 --- a/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_tcp_port_22/ec2_networkacl_allow_ingress_tcp_port_22.py +++ b/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_tcp_port_22/ec2_networkacl_allow_ingress_tcp_port_22.py @@ -8,7 +8,7 @@ class ec2_networkacl_allow_ingress_tcp_port_22(Check): findings = [] tcp_protocol = "6" check_port = 22 - for network_acl in ec2_client.network_acls: + for arn, network_acl in ec2_client.network_acls.items(): if ( ec2_client.provider.scan_unused_services or network_acl.region in ec2_client.regions_with_sgs @@ -18,7 +18,7 @@ class ec2_networkacl_allow_ingress_tcp_port_22(Check): report = Check_Report_AWS(self.metadata()) report.resource_id = network_acl.id report.region = network_acl.region - report.resource_arn = network_acl.arn + report.resource_arn = arn report.resource_tags = network_acl.tags report.status = "FAIL" report.status_extended = f"Network ACL {network_acl.name if network_acl.name else network_acl.id} has SSH port 22 open to the Internet." diff --git a/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_tcp_port_3389/ec2_networkacl_allow_ingress_tcp_port_3389.py b/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_tcp_port_3389/ec2_networkacl_allow_ingress_tcp_port_3389.py index 11adad1d69..580a1f75a8 100644 --- a/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_tcp_port_3389/ec2_networkacl_allow_ingress_tcp_port_3389.py +++ b/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_tcp_port_3389/ec2_networkacl_allow_ingress_tcp_port_3389.py @@ -8,7 +8,7 @@ class ec2_networkacl_allow_ingress_tcp_port_3389(Check): findings = [] tcp_protocol = "6" check_port = 3389 - for network_acl in ec2_client.network_acls: + for arn, network_acl in ec2_client.network_acls.items(): if ( ec2_client.provider.scan_unused_services or network_acl.region in ec2_client.regions_with_sgs @@ -18,7 +18,7 @@ class ec2_networkacl_allow_ingress_tcp_port_3389(Check): report = Check_Report_AWS(self.metadata()) report.resource_id = network_acl.id report.region = network_acl.region - report.resource_arn = network_acl.arn + report.resource_arn = arn report.resource_tags = network_acl.tags report.status = "FAIL" report.status_extended = f"Network ACL {network_acl.name if network_acl.name else network_acl.id} has Microsoft RDP port 3389 open to the Internet." diff --git a/prowler/providers/aws/services/ec2/ec2_networkacl_unused/__init__.py b/prowler/providers/aws/services/ec2/ec2_networkacl_unused/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused.metadata.json b/prowler/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused.metadata.json new file mode 100644 index 0000000000..ec120d7780 --- /dev/null +++ b/prowler/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "ec2_networkacl_unused", + "CheckTitle": "Unused Network Access Control Lists should be removed.", + "CheckType": [], + "ServiceName": "ec2", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "Severity": "low", + "ResourceType": "AwsEc2NetworkAcl", + "Description": "Ensure that there are no unused network access control lists (network ACLs) in your virtual private cloud (VPC). The control fails if the network ACL isn't associated with a subnet. The control doesn't generate findings for an unused default network ACL.", + "Risk": "Unused network ACLs may represent a potential security risk if left in place without purpose, as they could be mistakenly associated with subnets later.", + "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/vpc-network-acl-unused-check.html", + "Remediation": { + "Code": { + "CLI": "aws ec2 delete-network-acl --network-acl-id ", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ec2-controls.html#ec2-16", + "Terraform": "" + }, + "Recommendation": { + "Text": "For instructions on deleting an unused network ACL, see Deleting a network ACL in the Amazon VPC User Guide.", + "Url": "https://docs.aws.amazon.com/vpc/latest/userguide/vpc-network-acls.html#vpc-network-acl-delete" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Infrastructure Security" +} diff --git a/prowler/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused.py b/prowler/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused.py new file mode 100644 index 0000000000..036cb8036e --- /dev/null +++ b/prowler/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused.py @@ -0,0 +1,25 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.ec2.ec2_client import ec2_client + + +class ec2_networkacl_unused(Check): + def execute(self): + findings = [] + for arn, network_acl in ec2_client.network_acls.items(): + if not network_acl.default: + report = Check_Report_AWS(self.metadata()) + report.resource_id = network_acl.id + report.region = network_acl.region + report.resource_arn = arn + report.resource_tags = network_acl.tags + + if not network_acl.in_use: + report.status = "FAIL" + report.status_extended = f"Network ACL {network_acl.name if network_acl.name else network_acl.id} is not associated with any subnet and is not the default network ACL." + else: + report.status = "PASS" + report.status_extended = f"Network ACL {network_acl.name if network_acl.name else network_acl.id} is associated with a subnet." + + 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 c90943b932..149730a6ca 100644 --- a/prowler/providers/aws/services/ec2/ec2_service.py +++ b/prowler/providers/aws/services/ec2/ec2_service.py @@ -22,7 +22,7 @@ class EC2(AWSService): self.security_groups = {} self.regions_with_sgs = [] self.__threading_call__(self._describe_security_groups) - self.network_acls = [] + self.network_acls = {} self.__threading_call__(self._describe_network_acls) self.snapshots = [] self.volumes_with_snapshots = {} @@ -157,15 +157,20 @@ class EC2(AWSService): for tag in nacl.get("Tags", []): if tag["Key"] == "Name": nacl_name = tag["Value"] - self.network_acls.append( - NetworkACL( - id=nacl["NetworkAclId"], - arn=arn, - name=nacl_name, - region=regional_client.region, - entries=nacl["Entries"], - tags=nacl.get("Tags"), - ) + in_use = False + for subnet in nacl["Associations"]: + if subnet["SubnetId"]: + in_use = True + break + self.network_acls[arn] = NetworkACL( + id=nacl["NetworkAclId"], + arn=arn, + name=nacl_name, + region=regional_client.region, + entries=nacl["Entries"], + tags=nacl.get("Tags"), + in_use=in_use, + default=nacl["IsDefault"], ) except Exception as error: logger.error( @@ -684,6 +689,8 @@ class NetworkACL(BaseModel): name: str region: str entries: list[dict] + default: bool + in_use: bool tags: Optional[list] = [] diff --git a/tests/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused_test.py b/tests/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused_test.py new file mode 100644 index 0000000000..b141f51d0a --- /dev/null +++ b/tests/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused_test.py @@ -0,0 +1,126 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_REGION_EU_WEST_1, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_ec2_networkacl_unused: + @mock_aws + def test_ec2_default_nacls(self): + 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, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_networkacl_unused.ec2_networkacl_unused.ec2_client", + new=EC2(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_networkacl_unused.ec2_networkacl_unused import ( + ec2_networkacl_unused, + ) + + check = ec2_networkacl_unused() + result = check.execute() + + # One default nacl per region + assert len(result) == 0 + + @mock_aws + def test_ec2_unused_non_default_nacl(self): + # Create EC2 Mocked Resources + ec2_client = client("ec2", region_name=AWS_REGION_EU_WEST_1) + vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"] + nacl_id = ec2_client.create_network_acl(VpcId=vpc_id)["NetworkAcl"][ + "NetworkAclId" + ] + arn = f"arn:aws:ec2:eu-west-1:123456789012:network-acl/{nacl_id}" + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + 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.ec2.ec2_networkacl_unused.ec2_networkacl_unused.ec2_client", + new=EC2(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_networkacl_unused.ec2_networkacl_unused import ( + ec2_networkacl_unused, + ) + + check = ec2_networkacl_unused() + result = check.execute() + + assert len(result) == 1 + + assert result[0].status == "FAIL" + assert result[0].region == "eu-west-1" + assert result[0].resource_id == nacl_id + assert result[0].resource_arn == arn + assert ( + result[0].status_extended + == f"Network ACL {nacl_id} is not associated with any subnet and is not the default network ACL." + ) + + @mock_aws + def test_ec2_used_non_default_nacl(self): + # Create EC2 Mocked Resources + ec2_client = client("ec2", region_name=AWS_REGION_EU_WEST_1) + vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"] + nacl_id = ec2_client.create_network_acl(VpcId=vpc_id)["NetworkAcl"][ + "NetworkAclId" + ] + arn = f"arn:aws:ec2:eu-west-1:123456789012:network-acl/{nacl_id}" + + default_nacl_association_id = ec2_client.describe_network_acls()["NetworkAcls"][ + 0 + ]["Associations"][0]["NetworkAclAssociationId"] + # Replace the default association with the new Network ACL + ec2_client.replace_network_acl_association( + AssociationId=default_nacl_association_id, NetworkAclId=nacl_id + ) + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + 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.ec2.ec2_networkacl_unused.ec2_networkacl_unused.ec2_client", + new=EC2(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_networkacl_unused.ec2_networkacl_unused import ( + ec2_networkacl_unused, + ) + + check = ec2_networkacl_unused() + result = check.execute() + + assert len(result) == 1 + # Search changed sg + assert result[0].status == "PASS" + assert result[0].region == "eu-west-1" + assert result[0].resource_id == nacl_id + assert result[0].resource_arn == arn + assert ( + result[0].status_extended + == f"Network ACL {nacl_id} is associated with a subnet." + ) diff --git a/tests/providers/aws/services/ec2/ec2_service_test.py b/tests/providers/aws/services/ec2/ec2_service_test.py index 46786b46f8..f8b8eb7a96 100644 --- a/tests/providers/aws/services/ec2/ec2_service_test.py +++ b/tests/providers/aws/services/ec2/ec2_service_test.py @@ -240,14 +240,16 @@ class Test_EC2_Service: ec2 = EC2(aws_provider) assert nacl_id in str(ec2.network_acls) - for acl in ec2.network_acls: + for arn, acl in ec2.network_acls.items(): if acl.id == nacl_id: assert re.match(r"acl-[0-9a-z]{8}", acl.id) assert ( - acl.arn + arn == f"arn:{aws_provider.identity.partition}:ec2:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:network-acl/{acl.id}" ) assert acl.entries == [] + assert not acl.in_use + assert not acl.default assert acl.tags == [ {"Key": "test", "Value": "test"}, ]