diff --git a/prowler/providers/aws/services/ecs/ecs_service.py b/prowler/providers/aws/services/ecs/ecs_service.py index b29d3d7f90..3d0f531269 100644 --- a/prowler/providers/aws/services/ecs/ecs_service.py +++ b/prowler/providers/aws/services/ecs/ecs_service.py @@ -47,7 +47,7 @@ class ECS(AWSService): ) def _describe_task_definition(self, task_definition): - logger.info("ECS - Describing Task Definitions...") + logger.info("ECS - Describing Task Definition...") try: client = self.regional_clients[task_definition.region] response = client.describe_task_definition( @@ -70,6 +70,9 @@ class ECS(AWSService): ContainerDefinition( name=container["name"], privileged=container.get("privileged", False), + readonly_rootfilesystem=container.get( + "readonlyRootFilesystem", False + ), user=container.get("user", ""), environment=environment, ) @@ -164,6 +167,7 @@ class ContainerEnvVariable(BaseModel): class ContainerDefinition(BaseModel): name: str privileged: bool + readonly_rootfilesystem: bool = False user: str environment: list[ContainerEnvVariable] diff --git a/prowler/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/__init__.py b/prowler/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access.metadata.json b/prowler/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access.metadata.json new file mode 100644 index 0000000000..9e320892f6 --- /dev/null +++ b/prowler/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "aws", + "CheckID": "ecs_task_definitions_containers_readonly_access", + "CheckTitle": "ECS containers should be limited to read-only access to root filesystems", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "ecs", + "SubServiceName": "taskDefinition", + "ResourceIdTemplate": "arn:aws:ecs:{region}:{account-id}:task-definition/{task-definition-name}", + "Severity": "high", + "ResourceType": "AwsEcsTaskDefinition", + "Description": "This control checks if Amazon ECS containers are limited to read-only access to mounted root filesystems. The control fails if the readonlyRootFilesystem parameter is set to false or if the parameter doesn't exist in the container definition.", + "Risk": "If ECS containers have write access to root filesystems, it increases the risk of filesystem tampering and exploitation of vulnerabilities, violating the principle of least privilege.", + "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/ecs-containers-readonly-access.html", + "Remediation": { + "Code": { + "CLI": "aws ecs register-task-definition --family --container-definitions '[{\"name\":\"\",\"image\":\"\",\"readonlyRootFilesystem\":true}]'", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ecs-controls.html#ecs-5", + "Terraform": "" + }, + "Recommendation": { + "Text": "Ensure that containers have read-only access to their root filesystems to limit write access and reduce the attack surface.", + "Url": "https://docs.aws.amazon.com/AmazonECS/latest/userguide/task_definition_parameters.html#container_definition_readonly" + } + }, + "Categories": [ + "vulnerabilities" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access.py b/prowler/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access.py new file mode 100644 index 0000000000..fce00a1d68 --- /dev/null +++ b/prowler/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access.py @@ -0,0 +1,27 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.ecs.ecs_client import ecs_client + + +class ecs_task_definitions_containers_readonly_access(Check): + def execute(self): + findings = [] + for task_definition in ecs_client.task_definitions.values(): + report = Check_Report_AWS(self.metadata()) + report.region = task_definition.region + report.resource_id = f"{task_definition.name}:{task_definition.revision}" + report.resource_arn = task_definition.arn + report.resource_tags = task_definition.tags + report.status = "PASS" + report.status_extended = f"ECS task definition {task_definition.name} does not have containers with write access to the root filesystems." + + failed_containers = [] + for container in task_definition.container_definitions: + if not container.readonly_rootfilesystem: + report.status = "FAIL" + failed_containers.append(container.name) + + if failed_containers: + report.status_extended = f"ECS task definition {task_definition.name} has containers with write access to the root filesystem: {', '.join(failed_containers)}" + findings.append(report) + + return findings diff --git a/tests/providers/aws/services/ecs/ecs_service_test.py b/tests/providers/aws/services/ecs/ecs_service_test.py index 68b83ab0cd..b83880e66c 100644 --- a/tests/providers/aws/services/ecs/ecs_service_test.py +++ b/tests/providers/aws/services/ecs/ecs_service_test.py @@ -157,6 +157,11 @@ class Test_ECS_Service: assert ecs.task_definitions[task_arn].network_mode == "host" assert not ecs.task_definitions[task_arn].container_definitions[0].privileged assert ecs.task_definitions[task_arn].container_definitions[0].user == "" + assert ( + not ecs.task_definitions[task_arn] + .container_definitions[0] + .readonly_rootfilesystem + ) # Test list ECS clusters @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) diff --git a/tests/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access_test.py b/tests/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access_test.py new file mode 100644 index 0000000000..bfedf54e3c --- /dev/null +++ b/tests/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access_test.py @@ -0,0 +1,149 @@ +from unittest import mock + +from prowler.providers.aws.services.ecs.ecs_service import ( + ContainerDefinition, + TaskDefinition, +) +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_US_EAST_1 + +TASK_NAME = "test-task-readonly" +TASK_REVISION = "1" +CONTAINER_NAME = "test-container" +TASK_ARN = f"arn:aws:ecs:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:task-definition/{TASK_NAME}:{TASK_REVISION}" + + +class Test_ecs_task_definitions_containers_readonly_access: + def test_no_task_definitions(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = {} + + with mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_containers_readonly_access.ecs_task_definitions_containers_readonly_access import ( + ecs_task_definitions_containers_readonly_access, + ) + + check = ecs_task_definitions_containers_readonly_access() + result = check.execute() + assert len(result) == 0 + + def test_task_definition_all_containers_readonly(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = {} + ecs_client.task_definitions[TASK_ARN] = TaskDefinition( + name=TASK_NAME, + arn=TASK_ARN, + revision=TASK_REVISION, + region=AWS_REGION_US_EAST_1, + network_mode="bridge", + container_definitions=[ + ContainerDefinition( + name=CONTAINER_NAME, + readonly_rootfilesystem=True, + privileged=False, + user="appuser", + environment=[], + ) + ], + ) + + with mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_containers_readonly_access.ecs_task_definitions_containers_readonly_access import ( + ecs_task_definitions_containers_readonly_access, + ) + + check = ecs_task_definitions_containers_readonly_access() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"ECS task definition {TASK_NAME} does not have containers with write access to the root filesystems." + ) + + def test_task_definition_some_containers_not_readonly(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = {} + ecs_client.task_definitions[TASK_ARN] = TaskDefinition( + name=TASK_NAME, + arn=TASK_ARN, + revision=TASK_REVISION, + region=AWS_REGION_US_EAST_1, + network_mode="bridge", + container_definitions=[ + ContainerDefinition( + name=CONTAINER_NAME, + readonly_rootfilesystem=False, + privileged=False, + user="appuser", + environment=[], + ) + ], + ) + + with mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_containers_readonly_access.ecs_task_definitions_containers_readonly_access import ( + ecs_task_definitions_containers_readonly_access, + ) + + check = ecs_task_definitions_containers_readonly_access() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"ECS task definition {TASK_NAME} has containers with write access to the root filesystem: {CONTAINER_NAME}" + ) + + def test_task_definition_mixed_containers(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = { + TASK_ARN: TaskDefinition( + name=TASK_NAME, + arn=TASK_ARN, + revision=TASK_REVISION, + region=AWS_REGION_US_EAST_1, + network_mode="bridge", + container_definitions=[ + ContainerDefinition( + name=CONTAINER_NAME, + readonly_rootfilesystem=False, + privileged=False, + user="appuser", + environment=[], + ), + ContainerDefinition( + name="readonly-container", + readonly_rootfilesystem=True, + privileged=False, + user="appuser", + environment=[], + ), + ], + ) + } + + with mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_containers_readonly_access.ecs_task_definitions_containers_readonly_access import ( + ecs_task_definitions_containers_readonly_access, + ) + + check = ecs_task_definitions_containers_readonly_access() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"ECS task definition {TASK_NAME} has containers with write access to the root filesystem: {CONTAINER_NAME}" + ) diff --git a/tests/providers/aws/services/ecs/ecs_task_definitions_user_and_container_for_host_mode/ecs_task_definitions_user_and_container_for_host_mode_test.py b/tests/providers/aws/services/ecs/ecs_task_definitions_host_networking_mode_users/ecs_task_definitions_host_networking_mode_users_test.py similarity index 81% rename from tests/providers/aws/services/ecs/ecs_task_definitions_user_and_container_for_host_mode/ecs_task_definitions_user_and_container_for_host_mode_test.py rename to tests/providers/aws/services/ecs/ecs_task_definitions_host_networking_mode_users/ecs_task_definitions_host_networking_mode_users_test.py index 92542ad74f..82df0b9791 100644 --- a/tests/providers/aws/services/ecs/ecs_task_definitions_user_and_container_for_host_mode/ecs_task_definitions_user_and_container_for_host_mode_test.py +++ b/tests/providers/aws/services/ecs/ecs_task_definitions_host_networking_mode_users/ecs_task_definitions_host_networking_mode_users_test.py @@ -7,10 +7,10 @@ from prowler.providers.aws.services.ecs.ecs_service import ( ) from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_US_EAST_1 -task_name = "test-task" -task_revision = "1" -container_name = "test-container" -task_arn = f"arn:aws:ecs:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:task-definition/{task_name}:{task_revision}" +TASK_NAME = "test-task-hostmode" +TASK_REVISION = "1" +CONTAINER_NAME = "test-container" +TASK_ARN = f"arn:aws:ecs:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:task-definition/{TASK_NAME}:{TASK_REVISION}" class Test_ecs_task_definitions_host_networking_mode_users: @@ -33,15 +33,15 @@ class Test_ecs_task_definitions_host_networking_mode_users: def test_task_definition_no_host_network_mode(self): ecs_client = mock.MagicMock ecs_client.task_definitions = {} - ecs_client.task_definitions[task_arn] = TaskDefinition( - name=task_name, - arn=task_arn, - revision=task_revision, + ecs_client.task_definitions[TASK_ARN] = TaskDefinition( + name=TASK_NAME, + arn=TASK_ARN, + revision=TASK_REVISION, region=AWS_REGION_US_EAST_1, network_mode="bridge", container_definitions=[ ContainerDefinition( - name=container_name, + name=CONTAINER_NAME, privileged=False, user="", environment=[ @@ -68,21 +68,21 @@ class Test_ecs_task_definitions_host_networking_mode_users: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"ECS task definition {task_name} does not have host network mode." + == f"ECS task definition {TASK_NAME} does not have host network mode." ) def test_task_definition_host_mode_container_root_non_privileged(self): ecs_client = mock.MagicMock ecs_client.task_definitions = {} - ecs_client.task_definitions[task_arn] = TaskDefinition( - name=task_name, - arn=task_arn, - revision=task_revision, + ecs_client.task_definitions[TASK_ARN] = TaskDefinition( + name=TASK_NAME, + arn=TASK_ARN, + revision=TASK_REVISION, region=AWS_REGION_US_EAST_1, network_mode="host", container_definitions=[ ContainerDefinition( - name=container_name, + name=CONTAINER_NAME, privileged=False, user="root", environment=[], @@ -104,21 +104,21 @@ class Test_ecs_task_definitions_host_networking_mode_users: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"ECS task definition {task_name} has containers with host network mode and non-privileged containers running as root or with no user specified: {container_name}" + == f"ECS task definition {TASK_NAME} has containers with host network mode and non-privileged containers running as root or with no user specified: {CONTAINER_NAME}" ) def test_task_definition_host_mode_container_privileged(self): ecs_client = mock.MagicMock ecs_client.task_definitions = {} - ecs_client.task_definitions[task_arn] = TaskDefinition( - name=task_name, - arn=f"arn:aws:ecs:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:task-definition/{task_name}:{task_revision}", - revision=task_revision, + ecs_client.task_definitions[TASK_ARN] = TaskDefinition( + name=TASK_NAME, + arn=f"arn:aws:ecs:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:task-definition/{TASK_NAME}:{TASK_REVISION}", + revision=TASK_REVISION, region=AWS_REGION_US_EAST_1, network_mode="host", container_definitions=[ ContainerDefinition( - name=container_name, + name=CONTAINER_NAME, privileged=True, user="root", environment=[], @@ -140,21 +140,21 @@ class Test_ecs_task_definitions_host_networking_mode_users: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"ECS task definition {task_name} has host network mode but no containers running as root or with no user specified." + == f"ECS task definition {TASK_NAME} has host network mode but no containers running as root or with no user specified." ) def test_task_definition_host_mode_container_not_root(self): ecs_client = mock.MagicMock ecs_client.task_definitions = {} - ecs_client.task_definitions[task_arn] = TaskDefinition( - name=task_name, - arn=task_arn, - revision=task_revision, + ecs_client.task_definitions[TASK_ARN] = TaskDefinition( + name=TASK_NAME, + arn=TASK_ARN, + revision=TASK_REVISION, region=AWS_REGION_US_EAST_1, network_mode="host", container_definitions=[ ContainerDefinition( - name=container_name, + name=CONTAINER_NAME, privileged=False, user="appuser", environment=[], @@ -176,5 +176,5 @@ class Test_ecs_task_definitions_host_networking_mode_users: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"ECS task definition {task_name} has host network mode but no containers running as root or with no user specified." + == f"ECS task definition {TASK_NAME} has host network mode but no containers running as root or with no user specified." )