From 67262f35e607267d02d4f257e36441cc3fe69a8d Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 19 Sep 2022 12:50:08 -0700 Subject: [PATCH] aws - es - add "has-statement" filter (#7751) --- c7n/resources/elasticsearch.py | 15 ++++ .../es.DescribeElasticsearchDomains_1.json | 84 +++++++++++++++++++ .../es.ListDomainNames_1.json | 12 +++ .../es.ListTags_1.json | 7 ++ tests/test_elasticsearch.py | 56 +++++++++++++ 5 files changed, 174 insertions(+) create mode 100644 tests/data/placebo/test_elasticsearch_has_statement/es.DescribeElasticsearchDomains_1.json create mode 100644 tests/data/placebo/test_elasticsearch_has_statement/es.ListDomainNames_1.json create mode 100644 tests/data/placebo/test_elasticsearch_has_statement/es.ListTags_1.json diff --git a/c7n/resources/elasticsearch.py b/c7n/resources/elasticsearch.py index 40e7ee4bea5..52eb5f4c2bb 100644 --- a/c7n/resources/elasticsearch.py +++ b/c7n/resources/elasticsearch.py @@ -12,6 +12,7 @@ from c7n.utils import chunks, local_session, type_schema from c7n.tags import Tag, RemoveTag, TagActionFilter, TagDelayedAction from c7n.filters.kms import KmsRelatedFilter +import c7n.filters.policystatement as polstmt_filter from .securityhub import PostFinding @@ -209,6 +210,20 @@ def process(self, resources, event=None): return results +@ElasticSearchDomain.filter_registry.register('has-statement') +class HasStatementFilter(polstmt_filter.HasStatementFilter): + def __init__(self, data, manager=None): + super().__init__(data, manager) + self.policy_attribute = 'AccessPolicies' + + def get_std_format_args(self, domain): + return { + 'domain_arn': domain['ARN'], + 'account_id': self.manager.config.account_id, + 'region': self.manager.config.region + } + + @ElasticSearchDomain.action_registry.register('remove-statements') class RemovePolicyStatement(RemovePolicyBase): """ diff --git a/tests/data/placebo/test_elasticsearch_has_statement/es.DescribeElasticsearchDomains_1.json b/tests/data/placebo/test_elasticsearch_has_statement/es.DescribeElasticsearchDomains_1.json new file mode 100644 index 00000000000..520565a9ec7 --- /dev/null +++ b/tests/data/placebo/test_elasticsearch_has_statement/es.DescribeElasticsearchDomains_1.json @@ -0,0 +1,84 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "DomainStatusList": [ + { + "DomainId": "644160558196/my-test-cluster", + "DomainName": "my-test-cluster", + "ARN": "arn:aws:es:us-east-1:644160558196:domain/my-test-cluster", + "Created": true, + "Deleted": false, + "Processing": true, + "UpgradeProcessing": false, + "ElasticsearchVersion": "OpenSearch_1.3", + "ElasticsearchClusterConfig": { + "InstanceType": "t3.small.elasticsearch", + "InstanceCount": 1, + "DedicatedMasterEnabled": false, + "ZoneAwarenessEnabled": false, + "WarmEnabled": false, + "ColdStorageOptions": { + "Enabled": false + } + }, + "EBSOptions": { + "EBSEnabled": true, + "VolumeType": "gp3", + "VolumeSize": 10, + "Iops": 3000 + }, + "AccessPolicies": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Deny\",\"Principal\":{\"AWS\":\"*\"},\"Action\":\"es:*\",\"Resource\":\"arn:aws:es:us-east-1:644160558196:domain/my-test-cluster/*\"}]}", + "SnapshotOptions": {}, + "CognitoOptions": { + "Enabled": false + }, + "EncryptionAtRestOptions": { + "Enabled": true, + "KmsKeyId": "arn:aws:kms:us-east-1:644160558196:key/0215f288-e10e-44fd-b4d3-31e1685af8e6" + }, + "NodeToNodeEncryptionOptions": { + "Enabled": true + }, + "AdvancedOptions": { + "indices.fielddata.cache.size": "20", + "indices.query.bool.max_clause_count": "1024", + "override_main_response_version": "false", + "rest.action.multi.allow_explicit_index": "true" + }, + "ServiceSoftwareOptions": { + "CurrentVersion": "", + "NewVersion": "", + "UpdateAvailable": false, + "Cancellable": false, + "UpdateStatus": "COMPLETED", + "Description": "There is no software update available for this domain.", + "AutomatedUpdateDate": { + "__class__": "datetime", + "year": 1969, + "month": 12, + "day": 31, + "hour": 16, + "minute": 0, + "second": 0, + "microsecond": 0 + }, + "OptionalDeployment": true + }, + "DomainEndpointOptions": { + "EnforceHTTPS": true, + "TLSSecurityPolicy": "Policy-Min-TLS-1-0-2019-07", + "CustomEndpointEnabled": false + }, + "AdvancedSecurityOptions": { + "Enabled": true, + "InternalUserDatabaseEnabled": true, + "AnonymousAuthEnabled": false + }, + "AutoTuneOptions": { + "State": "ENABLE_IN_PROGRESS" + } + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_elasticsearch_has_statement/es.ListDomainNames_1.json b/tests/data/placebo/test_elasticsearch_has_statement/es.ListDomainNames_1.json new file mode 100644 index 00000000000..172bb0dcb9d --- /dev/null +++ b/tests/data/placebo/test_elasticsearch_has_statement/es.ListDomainNames_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "DomainNames": [ + { + "DomainName": "my-test-cluster", + "EngineType": "OpenSearch" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_elasticsearch_has_statement/es.ListTags_1.json b/tests/data/placebo/test_elasticsearch_has_statement/es.ListTags_1.json new file mode 100644 index 00000000000..db3be6e3f6f --- /dev/null +++ b/tests/data/placebo/test_elasticsearch_has_statement/es.ListTags_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "TagList": [] + } +} \ No newline at end of file diff --git a/tests/test_elasticsearch.py b/tests/test_elasticsearch.py index 78b0a083881..738dc96ff36 100644 --- a/tests/test_elasticsearch.py +++ b/tests/test_elasticsearch.py @@ -507,6 +507,62 @@ def test_remove_statements_validation_error(self): } ) + def test_elasticsearch_has_statement(self): + factory = self.replay_flight_data("test_elasticsearch_has_statement") + p = self.load_policy( + { + "name": "elasticsearch-has-statement-deny", + "resource": "elasticsearch", + "filters": [ + { + "type": "has-statement", + "statements": [ + { + "Effect": "Deny", + "Action": "es:*", + "Principal": {"AWS": "*"}, + "Resource": "{domain_arn}/*" + } + ] + } + ], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + access_policy = json.loads(resources[0]['AccessPolicies']) + self.assertEqual(access_policy['Statement'][0]['Effect'], 'Deny') + self.assertEqual(access_policy['Statement'][0]['Action'], 'es:*') + self.assertEqual(access_policy['Statement'][0]['Principal'], {"AWS": "*"}) + self.assertEqual(access_policy['Statement'][0]['Resource'], + 'arn:aws:es:us-east-1:644160558196:domain/my-test-cluster/*') + + def test_elasticsearch_not_has_statement(self): + factory = self.replay_flight_data("test_elasticsearch_has_statement") + p = self.load_policy( + { + "name": "elasticsearch-has-statement-allow", + "resource": "elasticsearch", + "filters": [ + { + "type": "has-statement", + "statements": [ + { + "Effect": "Allow", + "Action": "es:*", + "Principal": {"AWS": "*"}, + "Resource": "{domain_arn}/*" + } + ] + } + ], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 0) + class TestReservedInstances(BaseTest):