diff --git a/prowler/providers/aws/services/opensearch/opensearch_domain_data_nodes_fault_tolerant/opensearch_domain_data_nodes_fault_tolerant.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_domain_data_nodes_fault_tolerant/opensearch_domain_data_nodes_fault_tolerant.metadata.json index cd49712258..96eb609507 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_domain_data_nodes_fault_tolerant/opensearch_domain_data_nodes_fault_tolerant.metadata.json +++ b/prowler/providers/aws/services/opensearch/opensearch_domain_data_nodes_fault_tolerant/opensearch_domain_data_nodes_fault_tolerant.metadata.json @@ -1,15 +1,15 @@ { "Provider": "aws", "CheckID": "opensearch_domain_data_nodes_fault_tolerant", - "CheckTitle": "OpenSearch Domain should have at least three data nodes", + "CheckTitle": "OpenSearch Domain should have at least three data nodes and have multiple Availability Zones", "CheckType": [], "ServiceName": "opensearch", "SubServiceName": "", "ResourceIdTemplate": "arn:partition:es:region:account-id:domain/resource-id", "Severity": "medium", "ResourceType": "AwsOpenSearchServiceDomain", - "Description": "An OpenSearch domain requires at least three data nodes for high availability and fault-tolerance. Deploying an OpenSearch domain with at least three data nodes ensures cluster operations if a node fails.", - "Risk": "A single point of failure exists in the OpenSearch domain, which can lead to data loss or service disruption.", + "Description": "An OpenSearch domain requires at least three data nodes and should be distributed across multiple Availability Zones to ensure high availability and fault tolerance.", + "Risk": "Without at least three data nodes and distribution across multiple Availability Zones, the OpenSearch domain risks service disruptions and data loss during failures.", "RelatedUrl": "https://docs.aws.amazon.com/opensearch-service/latest/developerguide/createupdatedomains.html#createdomains", "Remediation": { "Code": { @@ -20,7 +20,7 @@ }, "Recommendation": { "Text": "Configure the OpenSearch domain with at least three data nodes to ensure high availability and fault tolerance.", - "Url": "https://docs.aws.amazon.com/opensearch-service/latest/developerguide/createupdatedomains.html#createdomains" + "Url": "https://docs.aws.amazon.com/opensearch-service/latest/developerguide/managedomains-multiaz.html" } }, "Categories": [ diff --git a/prowler/providers/aws/services/opensearch/opensearch_domain_data_nodes_fault_tolerant/opensearch_domain_data_nodes_fault_tolerant.py b/prowler/providers/aws/services/opensearch/opensearch_domain_data_nodes_fault_tolerant/opensearch_domain_data_nodes_fault_tolerant.py index d6b7a9549d..aba5c5b2d0 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_domain_data_nodes_fault_tolerant/opensearch_domain_data_nodes_fault_tolerant.py +++ b/prowler/providers/aws/services/opensearch/opensearch_domain_data_nodes_fault_tolerant/opensearch_domain_data_nodes_fault_tolerant.py @@ -14,11 +14,17 @@ class opensearch_domain_data_nodes_fault_tolerant(Check): report.region = domain.region report.resource_tags = domain.tags report.status = "PASS" - report.status_extended = f"Opensearch domain {domain.name} has {domain.data_nodes_count} data nodes." + report.status_extended = f"Opensearch domain {domain.name} has {domain.data_nodes_count} data nodes and zone awareness enabled." - if domain.data_nodes_count < 3: + if domain.data_nodes_count < 3 and not domain.zone_awareness: + report.status = "FAIL" + report.status_extended = f"Opensearch domain {domain.name} does not have at least 3 data nodes and does not have zone awareness enabled, which is recommended for fault tolerance." + elif domain.data_nodes_count < 3: report.status = "FAIL" report.status_extended = f"Opensearch domain {domain.name} does not have at least 3 data nodes, which is recommended for fault tolerance." + elif not domain.zone_awareness: + report.status = "FAIL" + report.status_extended = f"Opensearch domain {domain.name} does not have zone awareness enabled, which is recommended for fault tolerance." findings.append(report) diff --git a/prowler/providers/aws/services/opensearch/opensearch_service.py b/prowler/providers/aws/services/opensearch/opensearch_service.py index b85c924fa8..38de029ba6 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service.py +++ b/prowler/providers/aws/services/opensearch/opensearch_service.py @@ -146,6 +146,11 @@ class OpenSearchService(AWSService): .get("ClusterConfig", {}) .get("InstanceCount", 0) ) + domain.zone_awareness = ( + describe_domain["DomainStatus"] + .get("ClusterConfig", {}) + .get("ZoneAwarenessEnabled", False) + ) except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -191,4 +196,5 @@ class OpenSearchDomain(BaseModel): dedicated_master_enabled: Optional[bool] dedicated_master_count: Optional[int] data_nodes_count: Optional[int] + zone_awareness: Optional[bool] tags: Optional[list] = [] diff --git a/tests/providers/aws/services/opensearch/opensearch_domain_data_nodes_fault_tolerant/opensearch_domain_data_nodes_fault_tolerant_test.py b/tests/providers/aws/services/opensearch/opensearch_domain_data_nodes_fault_tolerant/opensearch_domain_data_nodes_fault_tolerant_test.py index 029558df32..f4cadbd127 100644 --- a/tests/providers/aws/services/opensearch/opensearch_domain_data_nodes_fault_tolerant/opensearch_domain_data_nodes_fault_tolerant_test.py +++ b/tests/providers/aws/services/opensearch/opensearch_domain_data_nodes_fault_tolerant/opensearch_domain_data_nodes_fault_tolerant_test.py @@ -40,6 +40,7 @@ class Test_opensearch_domain_data_nodes_fault_tolerant: ClusterConfig={ "InstanceCount": 1, "InstanceType": "m3.medium.search", + "ZoneAwarenessEnabled": True, }, TagList=[ {"Key": "test", "Value": "test"}, @@ -77,13 +78,57 @@ class Test_opensearch_domain_data_nodes_fault_tolerant: assert result[0].resource_tags == [{"Key": "test", "Value": "test"}] @mock_aws - def test_domain_with_three_data_nodes(self): + def test_domain_with_three_data_nodes_and_not_zone_awaraness(self): opensearch_client = client("opensearch", region_name=AWS_REGION_EU_WEST_1) domain_arn = opensearch_client.create_domain( DomainName=domain_name, ClusterConfig={ "InstanceCount": 3, "InstanceType": "m3.medium.search", + "ZoneAwarenessEnabled": False, + }, + TagList=[{"Key": "test", "Value": "test"}], + )["DomainStatus"]["ARN"] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + from prowler.providers.aws.services.opensearch.opensearch_service import ( + OpenSearchService, + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.opensearch.opensearch_domain_data_nodes_fault_tolerant.opensearch_domain_data_nodes_fault_tolerant.opensearch_client", + new=OpenSearchService(aws_provider), + ): + from prowler.providers.aws.services.opensearch.opensearch_domain_data_nodes_fault_tolerant.opensearch_domain_data_nodes_fault_tolerant import ( + opensearch_domain_data_nodes_fault_tolerant, + ) + + check = opensearch_domain_data_nodes_fault_tolerant() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Opensearch domain {domain_name} does not have zone awareness enabled, which is recommended for fault tolerance." + ) + assert result[0].resource_id == domain_name + assert result[0].resource_arn == domain_arn + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_tags == [{"Key": "test", "Value": "test"}] + + @mock_aws + def test_domain_with_three_data_nodes_and_zone_awaraness(self): + opensearch_client = client("opensearch", region_name=AWS_REGION_EU_WEST_1) + domain_arn = opensearch_client.create_domain( + DomainName=domain_name, + ClusterConfig={ + "InstanceCount": 3, + "InstanceType": "m3.medium.search", + "ZoneAwarenessEnabled": True, }, TagList=[{"Key": "test", "Value": "test"}], )["DomainStatus"]["ARN"] @@ -111,7 +156,7 @@ class Test_opensearch_domain_data_nodes_fault_tolerant: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Opensearch domain {domain_name} has 3 data nodes." + == f"Opensearch domain {domain_name} has 3 data nodes and zone awareness enabled." ) assert result[0].resource_id == domain_name assert result[0].resource_arn == domain_arn diff --git a/tests/providers/aws/services/opensearch/opensearch_service_test.py b/tests/providers/aws/services/opensearch/opensearch_service_test.py index 52dd76191c..101243b147 100644 --- a/tests/providers/aws/services/opensearch/opensearch_service_test.py +++ b/tests/providers/aws/services/opensearch/opensearch_service_test.py @@ -71,6 +71,7 @@ def mock_make_api_call(self, operation_name, kwarg): "DedicatedMasterEnabled": True, "DedicatedMasterCount": 1, "InstanceCount": 3, + "ZoneAwarenessEnabled": True, }, "CognitoOptions": {"Enabled": True}, "EncryptionAtRestOptions": {"Enabled": True}, @@ -180,6 +181,7 @@ class Test_OpenSearchService_Service: assert opensearch.opensearch_domains[0].dedicated_master_enabled assert opensearch.opensearch_domains[0].dedicated_master_count == 1 assert opensearch.opensearch_domains[0].data_nodes_count == 3 + assert opensearch.opensearch_domains[0].zone_awareness assert opensearch.opensearch_domains[0].tags == [ {"Key": "test", "Value": "test"}, ]