Repository: cloud-custodian/cloud-custodian

Issue:
aws - ebs encryption account setting

add a filter and action on account resource for ebs volume encryption by default.


Discussion:
I can work on this!
We could really use this feature as well

Resolving patch:
diff --git a/c7n/resources/account.py b/c7n/resources/account.py
--- a/c7n/resources/account.py
+++ b/c7n/resources/account.py
@@ -27,8 +27,10 @@
 from c7n.actions.securityhub import OtherResourcePostFinding
 from c7n.exceptions import PolicyValidationError
 from c7n.filters import Filter, FilterRegistry, ValueFilter
+from c7n.filters.kms import KmsRelatedFilter
 from c7n.filters.multiattr import MultiAttrFilter
 from c7n.filters.missing import Missing
+from c7n.query import QueryResourceManager
 from c7n.manager import ResourceManager, resources
 from c7n.utils import local_session, type_schema, generate_arn
 
@@ -57,6 +59,7 @@ class Account(ResourceManager):
 
     filter_registry = filters
     action_registry = actions
+    retry = staticmethod(QueryResourceManager.retry)
 
     class resource_type(object):
         id = 'account_id'
@@ -1173,6 +1176,110 @@ def process(self, resources):
         client.put_encryption_config(**req)
 
 
+@filters.register('default-ebs-encryption')
+class EbsEncryption(Filter):
+    """Filter an account by its ebs encryption status.
+
+    By default for key we match on the alias name for a key.
+
+    :example:
+
+    .. code-block:: yaml
+
+       policies:
+         - name: check-default-ebs-encryption
+           resource: aws.account
+           filters:
+            - type: default-ebs-encryption
+              key: "alias/aws/ebs"
+              state: true
+
+    It is also possible to match on specific key attributes (tags, origin)
+
+    :example:
+
+    .. code-block:: yaml
+
+       policies:
+         - name: check-ebs-encryption-key-origin
+           resource: aws.account
+           filters:
+            - type: default-ebs-encryption
+              key:
+                type: value
+                key: Origin
+                value: AWS_KMS
+              state: true
+    """
+    permissions = ('ec2:GetEbsEncryptionByDefault',)
+    schema = type_schema(
+        'default-ebs-encryption',
+        state={'type': 'boolean'},
+        key={'oneOf': [
+            {'$ref': '#/definitions/filters/value'},
+            {'type': 'string'}]})
+
+    def process(self, resources, event=None):
+        state = self.data.get('state', False)
+        client = local_session(self.manager.session_factory).client('ec2')
+        account_state = client.get_ebs_encryption_by_default().get(
+            'EbsEncryptionByDefault')
+        if account_state != state:
+            return []
+        if state and 'key' in self.data:
+            vfd = (isinstance(self.data['key'], dict) and
+                   self.data['key'] or {'c7n:AliasName': self.data['key']})
+            vf = KmsRelatedFilter(vfd, self.manager)
+            vf.RelatedIdsExpression = 'KmsKeyId'
+            vf.annotate = False
+            key = client.get_ebs_default_kms_key_id().get('KmsKeyId')
+            if not vf.process([{'KmsKeyId': key}]):
+                return []
+        return resources
+
+
+@actions.register('set-ebs-encryption')
+class SetEbsEncryption(BaseAction):
+    """Set AWS EBS default encryption on an account
+
+    :example:
+
+    .. code-block:: yaml
+
+       policies:
+         - name: set-default-ebs-encryption
+           resource: aws.account
+           filters:
+            - type: default-ebs-encryption
+              state: false
+           actions:
+            - type: set-ebs-encryption
+              state: true
+              key: alias/aws/ebs
+    """
+    permissions = ('ec2:EnableEbsEncryptionByDefault',
+                   'ec2:DisableEbsEncryptionByDefault')
+
+    schema = type_schema(
+        'set-ebs-encryption',
+        state={'type': 'boolean'},
+        key={'type': 'string'})
+
+    def process(self, resources):
+        client = local_session(
+            self.manager.session_factory).client('ec2')
+        state = self.data.get('state')
+        key = self.data.get('key')
+        if state:
+            client.enable_ebs_encryption_by_default()
+        else:
+            client.disable_ebs_encryption_by_default()
+
+        if state and key:
+            client.modify_ebs_default_kms_key_id(
+                KmsKeyId=self.data['key'])
+
+
 @filters.register('s3-public-block')
 class S3PublicBlock(ValueFilter):
     """Check for s3 public blocks on an account.


Test patch:
diff --git a/tests/data/placebo/test_account_disable_ebs_encrypt/ec2.DisableEbsEncryptionByDefault_1.json b/tests/data/placebo/test_account_disable_ebs_encrypt/ec2.DisableEbsEncryptionByDefault_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_account_disable_ebs_encrypt/ec2.DisableEbsEncryptionByDefault_1.json
@@ -0,0 +1,18 @@
+{
+    "status_code": 200,
+    "data": {
+        "EbsEncryptionByDefault": false,
+        "ResponseMetadata": {
+            "RequestId": "72811a77-0fb9-4d0f-85ff-e8db98064a6e",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "content-type": "text/xml;charset=UTF-8",
+                "transfer-encoding": "chunked",
+                "vary": "accept-encoding",
+                "date": "Wed, 10 Jul 2019 11:05:18 GMT",
+                "server": "AmazonEC2"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_account_disable_ebs_encrypt/ec2.GetEbsDefaultKmsKeyId_1.json b/tests/data/placebo/test_account_disable_ebs_encrypt/ec2.GetEbsDefaultKmsKeyId_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_account_disable_ebs_encrypt/ec2.GetEbsDefaultKmsKeyId_1.json
@@ -0,0 +1,18 @@
+{
+    "status_code": 200,
+    "data": {
+        "KmsKeyId": "arn:aws:kms:us-east-1:644160558196:key/798bc4bb-3079-4a9a-bc27-2c7f2b6c91d0",
+        "ResponseMetadata": {
+            "RequestId": "bf47e4af-9f9d-45b6-8fac-ac0a1262c6de",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "content-type": "text/xml;charset=UTF-8",
+                "transfer-encoding": "chunked",
+                "vary": "accept-encoding",
+                "date": "Wed, 10 Jul 2019 11:05:15 GMT",
+                "server": "AmazonEC2"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_account_disable_ebs_encrypt/ec2.GetEbsEncryptionByDefault_1.json b/tests/data/placebo/test_account_disable_ebs_encrypt/ec2.GetEbsEncryptionByDefault_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_account_disable_ebs_encrypt/ec2.GetEbsEncryptionByDefault_1.json
@@ -0,0 +1,18 @@
+{
+    "status_code": 200,
+    "data": {
+        "EbsEncryptionByDefault": true,
+        "ResponseMetadata": {
+            "RequestId": "5e2673f2-005f-48ce-95d1-0849a84d4ddf",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "content-type": "text/xml;charset=UTF-8",
+                "transfer-encoding": "chunked",
+                "vary": "accept-encoding",
+                "date": "Wed, 10 Jul 2019 11:05:15 GMT",
+                "server": "AmazonEC2"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_account_disable_ebs_encrypt/ec2.GetEbsEncryptionByDefault_2.json b/tests/data/placebo/test_account_disable_ebs_encrypt/ec2.GetEbsEncryptionByDefault_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_account_disable_ebs_encrypt/ec2.GetEbsEncryptionByDefault_2.json
@@ -0,0 +1,18 @@
+{
+    "status_code": 200,
+    "data": {
+        "EbsEncryptionByDefault": false,
+        "ResponseMetadata": {
+            "RequestId": "9db8875f-a49a-47c0-b47b-535148b9c4f0",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "content-type": "text/xml;charset=UTF-8",
+                "transfer-encoding": "chunked",
+                "vary": "accept-encoding",
+                "date": "Wed, 10 Jul 2019 11:05:17 GMT",
+                "server": "AmazonEC2"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_account_disable_ebs_encrypt/iam.ListAccountAliases_1.json b/tests/data/placebo/test_account_disable_ebs_encrypt/iam.ListAccountAliases_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_account_disable_ebs_encrypt/iam.ListAccountAliases_1.json
@@ -0,0 +1,20 @@
+{
+    "status_code": 200,
+    "data": {
+        "AccountAliases": [
+            "custodian-skunk-works"
+        ],
+        "IsTruncated": false,
+        "ResponseMetadata": {
+            "RequestId": "98d15b55-a302-11e9-81d8-f3678c6ee53c",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "98d15b55-a302-11e9-81d8-f3678c6ee53c",
+                "content-type": "text/xml",
+                "content-length": "400",
+                "date": "Wed, 10 Jul 2019 11:05:15 GMT"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_account_disable_ebs_encrypt/kms.DescribeKey_1.json b/tests/data/placebo/test_account_disable_ebs_encrypt/kms.DescribeKey_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_account_disable_ebs_encrypt/kms.DescribeKey_1.json
@@ -0,0 +1,40 @@
+{
+    "status_code": 200,
+    "data": {
+        "KeyMetadata": {
+            "AWSAccountId": "644160558196",
+            "KeyId": "798bc4bb-3079-4a9a-bc27-2c7f2b6c91d0",
+            "Arn": "arn:aws:kms:us-east-1:644160558196:key/798bc4bb-3079-4a9a-bc27-2c7f2b6c91d0",
+            "CreationDate": {
+                "__class__": "datetime",
+                "year": 2016,
+                "month": 6,
+                "day": 27,
+                "hour": 7,
+                "minute": 15,
+                "second": 48,
+                "microsecond": 139000
+            },
+            "Enabled": true,
+            "Description": "Default master key that protects my EBS volumes when no other key is defined",
+            "KeyUsage": "ENCRYPT_DECRYPT",
+            "KeyState": "Enabled",
+            "Origin": "AWS_KMS",
+            "KeyManager": "AWS"
+        },
+        "ResponseMetadata": {
+            "RequestId": "5b40f658-a8ad-48fc-abce-33cf4ddc7293",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "5b40f658-a8ad-48fc-abce-33cf4ddc7293",
+                "cache-control": "no-cache, no-store, must-revalidate, private",
+                "expires": "0",
+                "pragma": "no-cache",
+                "date": "Wed, 10 Jul 2019 11:05:17 GMT",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "436"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_account_disable_ebs_encrypt/kms.ListAliases_1.json b/tests/data/placebo/test_account_disable_ebs_encrypt/kms.ListAliases_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_account_disable_ebs_encrypt/kms.ListAliases_1.json
@@ -0,0 +1,27 @@
+{
+    "status_code": 200,
+    "data": {
+        "Aliases": [
+            {
+                "AliasName": "alias/aws/ebs",
+                "AliasArn": "arn:aws:kms:us-east-1:644160558196:alias/aws/ebs",
+                "TargetKeyId": "798bc4bb-3079-4a9a-bc27-2c7f2b6c91d0"
+            }
+        ],
+        "Truncated": false,
+        "ResponseMetadata": {
+            "RequestId": "cdfe0c77-302e-4682-b07a-f4c3398b02f0",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "cdfe0c77-302e-4682-b07a-f4c3398b02f0",
+                "cache-control": "no-cache, no-store, must-revalidate, private",
+                "expires": "0",
+                "pragma": "no-cache",
+                "date": "Wed, 10 Jul 2019 11:05:17 GMT",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "192"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_account_disable_ebs_encrypt/kms.ListKeys_1.json b/tests/data/placebo/test_account_disable_ebs_encrypt/kms.ListKeys_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_account_disable_ebs_encrypt/kms.ListKeys_1.json
@@ -0,0 +1,26 @@
+{
+    "status_code": 200,
+    "data": {
+        "Keys": [
+            {
+                "KeyId": "798bc4bb-3079-4a9a-bc27-2c7f2b6c91d0",
+                "KeyArn": "arn:aws:kms:us-east-1:644160558196:key/798bc4bb-3079-4a9a-bc27-2c7f2b6c91d0"
+            }
+        ],
+        "Truncated": false,
+        "ResponseMetadata": {
+            "RequestId": "a3f9f212-a8c1-49dc-856f-bb4967ad4840",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "a3f9f212-a8c1-49dc-856f-bb4967ad4840",
+                "cache-control": "no-cache, no-store, must-revalidate, private",
+                "expires": "0",
+                "pragma": "no-cache",
+                "date": "Wed, 10 Jul 2019 11:05:17 GMT",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "4802"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
diff --git a/tests/data/placebo/test_account_disable_ebs_encrypt/tagging.GetResources_1.json b/tests/data/placebo/test_account_disable_ebs_encrypt/tagging.GetResources_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_account_disable_ebs_encrypt/tagging.GetResources_1.json
@@ -0,0 +1,19 @@
+{
+    "status_code": 200,
+    "data": {
+        "PaginationToken": "",
+        "ResourceTagMappingList": [
+        ],
+        "ResponseMetadata": {
+            "RequestId": "a787059b-6ac1-4820-82ff-2fac926d42d8",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "a787059b-6ac1-4820-82ff-2fac926d42d8",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "470",
+                "date": "Wed, 10 Jul 2019 11:05:17 GMT"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
diff --git a/tests/data/placebo/test_account_ebs_encrypt/ec2.EnableEbsEncryptionByDefault_1.json b/tests/data/placebo/test_account_ebs_encrypt/ec2.EnableEbsEncryptionByDefault_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_account_ebs_encrypt/ec2.EnableEbsEncryptionByDefault_1.json
@@ -0,0 +1,18 @@
+{
+    "status_code": 200,
+    "data": {
+        "EbsEncryptionByDefault": true,
+        "ResponseMetadata": {
+            "RequestId": "d04acedd-1728-4117-afcb-ef5eb113382e",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "content-type": "text/xml;charset=UTF-8",
+                "transfer-encoding": "chunked",
+                "vary": "accept-encoding",
+                "date": "Wed, 10 Jul 2019 09:52:25 GMT",
+                "server": "AmazonEC2"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_account_ebs_encrypt/ec2.GetEbsEncryptionByDefault_1.json b/tests/data/placebo/test_account_ebs_encrypt/ec2.GetEbsEncryptionByDefault_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_account_ebs_encrypt/ec2.GetEbsEncryptionByDefault_1.json
@@ -0,0 +1,18 @@
+{
+    "status_code": 200,
+    "data": {
+        "EbsEncryptionByDefault": false,
+        "ResponseMetadata": {
+            "RequestId": "2dd77269-7c0c-453d-86d8-d12d9bba0b6e",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "content-type": "text/xml;charset=UTF-8",
+                "transfer-encoding": "chunked",
+                "vary": "accept-encoding",
+                "date": "Wed, 10 Jul 2019 09:52:26 GMT",
+                "server": "AmazonEC2"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_account_ebs_encrypt/ec2.GetEbsEncryptionByDefault_2.json b/tests/data/placebo/test_account_ebs_encrypt/ec2.GetEbsEncryptionByDefault_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_account_ebs_encrypt/ec2.GetEbsEncryptionByDefault_2.json
@@ -0,0 +1,18 @@
+{
+    "status_code": 200,
+    "data": {
+        "EbsEncryptionByDefault": true,
+        "ResponseMetadata": {
+            "RequestId": "3fab0909-ac4b-43c2-b789-4bc896e31d41",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "content-type": "text/xml;charset=UTF-8",
+                "transfer-encoding": "chunked",
+                "vary": "accept-encoding",
+                "date": "Wed, 10 Jul 2019 09:52:26 GMT",
+                "server": "AmazonEC2"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_account_ebs_encrypt/ec2.ModifyEbsDefaultKmsKeyId_1.json b/tests/data/placebo/test_account_ebs_encrypt/ec2.ModifyEbsDefaultKmsKeyId_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_account_ebs_encrypt/ec2.ModifyEbsDefaultKmsKeyId_1.json
@@ -0,0 +1,18 @@
+{
+    "status_code": 200,
+    "data": {
+        "KmsKeyId": "arn:aws:kms:us-east-1:644160558196:key/798bc4bb-3079-4a9a-bc27-2c7f2b6c91d0",
+        "ResponseMetadata": {
+            "RequestId": "d2864716-fa98-48e1-ad4c-67c60466f1ac",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "content-type": "text/xml;charset=UTF-8",
+                "transfer-encoding": "chunked",
+                "vary": "accept-encoding",
+                "date": "Wed, 10 Jul 2019 09:52:25 GMT",
+                "server": "AmazonEC2"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_account_ebs_encrypt/iam.ListAccountAliases_1.json b/tests/data/placebo/test_account_ebs_encrypt/iam.ListAccountAliases_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_account_ebs_encrypt/iam.ListAccountAliases_1.json
@@ -0,0 +1,20 @@
+{
+    "status_code": 200,
+    "data": {
+        "AccountAliases": [
+            "custodian-skunk-works"
+        ],
+        "IsTruncated": false,
+        "ResponseMetadata": {
+            "RequestId": "6c3b37b7-a2f8-11e9-932d-4b50a97db49e",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "6c3b37b7-a2f8-11e9-932d-4b50a97db49e",
+                "content-type": "text/xml",
+                "content-length": "400",
+                "date": "Wed, 10 Jul 2019 09:52:25 GMT"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/test_account.py b/tests/test_account.py
--- a/tests/test_account.py
+++ b/tests/test_account.py
@@ -84,6 +84,46 @@ def __call__(self, region=None, assume=None):
         resources = p.run()
         self.assertEqual(len(resources), 1)
 
+    def test_enable_encryption_by_default(self):
+        factory = self.replay_flight_data('test_account_ebs_encrypt')
+        p = self.load_policy({
+            'name': 'account',
+            'resource': 'account',
+            'filters': [{
+                'type': 'default-ebs-encryption',
+                'state': False}],
+            'actions': [{
+                'type': 'set-ebs-encryption',
+                'state': True,
+                'key': 'alias/aws/ebs'}]},
+            session_factory=factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        client = local_session(factory).client('ec2')
+        self.assertTrue(
+            client.get_ebs_encryption_by_default().get(
+                'EbsEncryptionByDefault'))
+
+    def test_disable_encryption_by_default(self):
+        factory = self.replay_flight_data('test_account_disable_ebs_encrypt')
+        p = self.load_policy({
+            'name': 'account',
+            'resource': 'account',
+            'filters': [{
+                'type': 'default-ebs-encryption',
+                'key': 'alias/aws/ebs',
+                'state': True}],
+            'actions': [{
+                'type': 'set-ebs-encryption',
+                'state': False}]},
+            session_factory=factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        client = local_session(factory).client('ec2')
+        self.assertFalse(
+            client.get_ebs_encryption_by_default().get(
+                'EbsEncryptionByDefault'))
+
     def test_guard_duty_filter(self):
         factory = self.replay_flight_data('test_account_guard_duty_filter')
         p = self.load_policy({