mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 20:42:02 +00:00
feat(ecs): Ensure ECS containers have read-only access to root filesystems (#5168)
Co-authored-by: Sergio <sergio@prowler.com>
This commit is contained in:
committed by
GitHub
parent
0474c7995c
commit
ff05ce4da1
@@ -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]
|
||||
|
||||
|
||||
+34
@@ -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 <task-family> --container-definitions '[{\"name\":\"<container-name>\",\"image\":\"<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": ""
|
||||
}
|
||||
+27
@@ -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
|
||||
@@ -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)
|
||||
|
||||
+149
@@ -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}"
|
||||
)
|
||||
+28
-28
@@ -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."
|
||||
)
|
||||
Reference in New Issue
Block a user