feat(ecs): Ensure ECS task definitions host's process namespace is not shared (#5146)

Co-authored-by: Sergio <sergio@prowler.com>
This commit is contained in:
Mario Rodriguez Lopez
2024-09-27 00:24:21 +02:00
committed by GitHub
parent ff05ce4da1
commit 9d4fa55c13
6 changed files with 224 additions and 1 deletions
@@ -77,8 +77,11 @@ class ECS(AWSService):
environment=environment,
)
)
task_definition.pid_mode = response["taskDefinition"].get("pidMode", "")
task_definition.tags = response.get("tags")
task_definition.network_mode = response["taskDefinition"].get("networkMode")
task_definition.network_mode = response["taskDefinition"].get(
"networkMode", "bridge"
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
@@ -178,6 +181,7 @@ class TaskDefinition(BaseModel):
revision: str
region: str
container_definitions: list[ContainerDefinition] = []
pid_mode: Optional[str]
tags: Optional[list] = []
network_mode: Optional[str]
@@ -0,0 +1,34 @@
{
"Provider": "aws",
"CheckID": "ecs_task_definitions_host_namespace_not_shared",
"CheckTitle": "ECS task definitions should not share the host's process namespace",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices"
],
"ServiceName": "ecs",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:ecs:{region}:{account-id}:task-definition/{task-definition-name}",
"Severity": "high",
"ResourceType": "AwsEcsTaskDefinition",
"Description": "This control checks if Amazon ECS task definitions are configured to share a host's process namespace with its containers. The control fails if the task definition shares the host's process namespace.",
"Risk": "Sharing the host's process namespace with containers exposes host processes, potentially allowing unauthorized access or manipulation of host-level processes, undermining container isolation.",
"RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/ecs-task-definition-pid-mode-check.html",
"Remediation": {
"Code": {
"CLI": "aws ecs register-task-definition --family <task-family> --container-definitions '[{\"name\":\"<container-name>\",\"image\":\"<image>\",\"pidMode\":\"task\"}]'",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ecs-controls.html#ecs-3",
"Terraform": ""
},
"Recommendation": {
"Text": "Avoid sharing the host's process namespace with containers to maintain process isolation and enhance security.",
"Url": "https://docs.aws.amazon.com/AmazonECS/latest/userguide/task_definition_parameters.html#container_definition_pid_mode"
}
},
"Categories": [
"vulnerabilities"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,20 @@
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_host_namespace_not_shared(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} with revision {task_definition.revision} does not share a host's process namespace with its containers."
if task_definition.pid_mode == "host":
report.status = "FAIL"
report.status_extended = f"ECS task definition {task_definition.name} with revision {task_definition.revision} is configured to share a host's process namespace with its containers."
findings.append(report)
return findings
@@ -28,6 +28,7 @@ def mock_make_api_call(self, operation_name, kwarg):
}
],
"networkMode": "host",
"pidMode": "host",
"tags": [],
}
}
@@ -157,6 +158,7 @@ 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 ecs.task_definitions[task_arn].pid_mode == "host"
assert (
not ecs.task_definitions[task_arn]
.container_definitions[0]
@@ -0,0 +1,163 @@
from unittest import mock
from prowler.providers.aws.services.ecs.ecs_service import (
ContainerDefinition,
ContainerEnvVariable,
TaskDefinition,
)
from tests.providers.aws.utils import (
AWS_ACCOUNT_NUMBER,
AWS_REGION_US_EAST_1,
set_mocked_aws_provider,
)
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}"
class Test_ecs_task_definitions_host_namespace_not_shared:
def test_no_task_definitions(self):
ecs_client = mock.MagicMock
ecs_client.task_definitions = {}
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
), mock.patch(
"prowler.providers.aws.services.ecs.ecs_service.ECS",
ecs_client,
):
from prowler.providers.aws.services.ecs.ecs_task_definitions_host_namespace_not_shared.ecs_task_definitions_host_namespace_not_shared import (
ecs_task_definitions_host_namespace_not_shared,
)
check = ecs_task_definitions_host_namespace_not_shared()
result = check.execute()
assert len(result) == 0
def test_task_definition_no_host_pid_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,
region=AWS_REGION_US_EAST_1,
network_mode="bridge",
pid_mode="task",
container_definitions=[
ContainerDefinition(
name=container_name,
privileged=False,
user="",
environment=[
ContainerEnvVariable(
name="env_var_name_no_secrets",
value="env_var_value_no_secrets",
)
],
)
],
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
), mock.patch(
"prowler.providers.aws.services.ecs.ecs_service.ECS",
ecs_client,
):
from prowler.providers.aws.services.ecs.ecs_task_definitions_host_namespace_not_shared.ecs_task_definitions_host_namespace_not_shared import (
ecs_task_definitions_host_namespace_not_shared,
)
check = ecs_task_definitions_host_namespace_not_shared()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"ECS task definition {task_name} with revision {task_revision} does not share a host's process namespace with its containers."
)
def test_task_definition_host_pid_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,
region=AWS_REGION_US_EAST_1,
network_mode="host",
pid_mode="host",
container_definitions=[
ContainerDefinition(
name=container_name,
privileged=False,
user="root",
environment=[],
)
],
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
), mock.patch(
"prowler.providers.aws.services.ecs.ecs_service.ECS",
ecs_client,
):
from prowler.providers.aws.services.ecs.ecs_task_definitions_host_namespace_not_shared.ecs_task_definitions_host_namespace_not_shared import (
ecs_task_definitions_host_namespace_not_shared,
)
check = ecs_task_definitions_host_namespace_not_shared()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"ECS task definition {task_name} with revision {task_revision} is configured to share a host's process namespace with its containers."
)
def test_task_definition_no_pid_mode(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,
region=AWS_REGION_US_EAST_1,
network_mode="host",
pid_mode="",
container_definitions=[
ContainerDefinition(
name=container_name,
privileged=True,
user="root",
environment=[],
)
],
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
), mock.patch(
"prowler.providers.aws.services.ecs.ecs_service.ECS",
ecs_client,
):
from prowler.providers.aws.services.ecs.ecs_task_definitions_host_namespace_not_shared.ecs_task_definitions_host_namespace_not_shared import (
ecs_task_definitions_host_namespace_not_shared,
)
check = ecs_task_definitions_host_namespace_not_shared()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"ECS task definition {task_name} with revision {task_revision} does not share a host's process namespace with its containers."
)