Repository: cloud-custodian/cloud-custodian

Issue:
rds-cluster-snapshot
**Is your feature request related to a problem? Please describe.**
No, It's an enhancement

**Describe the solution you'd like**
Need Cross-account filters on AWS RDS Cluster Snapshots.

**Describe alternatives you've considered**
I modified custodian code until it gets fixed.

**Additional context**

from c7n.filters import CrossAccountAccessFilter

@RDSClusterSnapshot.filter_registry.register('cross-account')
class CrossAccountAccess(CrossAccountAccessFilter):

    permissions = ('rds:DescribeDBClusterSnapshotAttributes',)

    def process(self, resources, event=None):
        self.accounts = self.get_accounts()
        results = []
        with self.executor_factory(max_workers=2) as w:
            futures = []
            for resource_set in chunks(resources, 20):
                futures.append(w.submit(
                    self.process_resource_set, resource_set))
            for f in as_completed(futures):
                if f.exception():
                    self.log.error(
                        "Exception checking cross account access\n %s" % (
                            f.exception()))
                    continue
                results.extend(f.result())
        return results

    def process_resource_set(self, resource_set):
        client = local_session(self.manager.session_factory).client('rds')
        results = []
        for r in resource_set:
            attrs = {t['AttributeName']: t['AttributeValues']
             #for t in client.describe_db_cluster_snapshot_attributes(
             for t in self.manager.retry(
                client.describe_db_cluster_snapshot_attributes,
                DBClusterSnapshotIdentifier=r['DBClusterSnapshotIdentifier'])[
                    'DBClusterSnapshotAttributesResult']['DBClusterSnapshotAttributes']}
            r['c7n:attributes'] = attrs
            shared_accounts = set(attrs.get('restore', []))
            delta_accounts = shared_accounts.difference(self.accounts)
            if delta_accounts:
                r['c7n:CrossAccountViolations'] = list(delta_accounts)
                results.append(r)
        return results



Discussion:
thanks, I'll see if can get this converted into a pr with some tests.
sure, Thank you, Kapilt

Resolving patch:
diff --git a/c7n/resources/rdscluster.py b/c7n/resources/rdscluster.py
--- a/c7n/resources/rdscluster.py
+++ b/c7n/resources/rdscluster.py
@@ -16,11 +16,11 @@
 from concurrent.futures import as_completed
 
 from c7n.actions import BaseAction
-from c7n.filters import AgeFilter
+from c7n.filters import AgeFilter, CrossAccountAccessFilter
 from c7n.filters.offhours import OffHour, OnHour
 import c7n.filters.vpc as net_filters
 from c7n.manager import resources
-from c7n.query import QueryResourceManager, TypeInfo
+from c7n.query import QueryResourceManager, TypeInfo, DescribeSource
 from c7n import tags
 from .aws import shape_validate
 from c7n.exceptions import PolicyValidationError
@@ -342,7 +342,6 @@ class RDSClusterSnapshot(QueryResourceManager):
     """
 
     class resource_type(TypeInfo):
-
         service = 'rds'
         arn_type = 'cluster-snapshot'
         arn_separator = ':'
@@ -352,9 +351,62 @@ class resource_type(TypeInfo):
         name = id = 'DBClusterSnapshotIdentifier'
         date = 'SnapshotCreateTime'
         universal_tagging = object()
+        config_type = 'AWS::RDS::DBClusterSnapshot'
         permissions_enum = ('rds:DescribeDBClusterSnapshots',)
 
-    augment = tags.universal_augment
+    def get_source(self, source_type):
+        if source_type == 'describe':
+            return DescribeClusterSnapshot(self)
+        return super().get_source(source_type)
+
+
+class DescribeClusterSnapshot(DescribeSource):
+
+    def get_resources(self, resource_ids, cache=True):
+        client = local_session(self.manager.session_factory).client('rds')
+        return self.manager.retry(
+            client.describe_db_cluster_snapshots,
+            Filters=[{
+                'Name': 'db-cluster-snapshot-id',
+                'Values': resource_ids}]).get('DBClusterSnapshots', ())
+
+    def augment(self, resources):
+        return tags.universal_augment(self.manager, resources)
+
+
+@RDSClusterSnapshot.filter_registry.register('cross-account')
+class CrossAccountSnapshot(CrossAccountAccessFilter):
+
+    permissions = ('rds:DescribeDBClusterSnapshotAttributes',)
+
+    def process(self, resources, event=None):
+        self.accounts = self.get_accounts()
+        results = []
+        with self.executor_factory(max_workers=2) as w:
+            futures = []
+            for resource_set in chunks(resources, 20):
+                futures.append(w.submit(
+                    self.process_resource_set, resource_set))
+            for f in as_completed(futures):
+                results.extend(f.result())
+        return results
+
+    def process_resource_set(self, resource_set):
+        client = local_session(self.manager.session_factory).client('rds')
+        results = []
+        for r in resource_set:
+            attrs = {t['AttributeName']: t['AttributeValues']
+             for t in self.manager.retry(
+                client.describe_db_cluster_snapshot_attributes,
+                     DBClusterSnapshotIdentifier=r['DBClusterSnapshotIdentifier'])[
+                         'DBClusterSnapshotAttributesResult']['DBClusterSnapshotAttributes']}
+            r['c7n:attributes'] = attrs
+            shared_accounts = set(attrs.get('restore', []))
+            delta_accounts = shared_accounts.difference(self.accounts)
+            if delta_accounts:
+                r['c7n:CrossAccountViolations'] = list(delta_accounts)
+                results.append(r)
+        return results
 
 
 @RDSClusterSnapshot.filter_registry.register('age')


Test patch:
diff --git a/tests/data/placebo/test_rds_cluster_snapshot_cross_account/rds.DescribeDBClusterSnapshotAttributes_1.json b/tests/data/placebo/test_rds_cluster_snapshot_cross_account/rds.DescribeDBClusterSnapshotAttributes_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rds_cluster_snapshot_cross_account/rds.DescribeDBClusterSnapshotAttributes_1.json
@@ -0,0 +1,17 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBClusterSnapshotAttributesResult": {
+            "DBClusterSnapshotIdentifier": "test-cluster-final-snapshot",
+            "DBClusterSnapshotAttributes": [
+                {
+                    "AttributeName": "restore",
+                    "AttributeValues": [
+                        "12345678910"
+                    ]
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
diff --git a/tests/data/placebo/test_rds_cluster_snapshot_cross_account/rds.DescribeDBClusterSnapshots_1.json b/tests/data/placebo/test_rds_cluster_snapshot_cross_account/rds.DescribeDBClusterSnapshots_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rds_cluster_snapshot_cross_account/rds.DescribeDBClusterSnapshots_1.json
@@ -0,0 +1,54 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBClusterSnapshots": [
+            {
+                "AvailabilityZones": [
+                    "us-east-1a",
+                    "us-east-1b",
+                    "us-east-1c",
+                    "us-east-1d",
+                    "us-east-1e",
+                    "us-east-1f"
+                ],
+                "DBClusterSnapshotIdentifier": "test-cluster-final-snapshot",
+                "DBClusterIdentifier": "test-cluster",
+                "SnapshotCreateTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 2,
+                    "day": 19,
+                    "hour": 15,
+                    "minute": 37,
+                    "second": 8,
+                    "microsecond": 707000
+                },
+                "Engine": "docdb",
+                "AllocatedStorage": 0,
+                "Status": "available",
+                "Port": 0,
+                "VpcId": "vpc-d2d616b5",
+                "ClusterCreateTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 1,
+                    "day": 15,
+                    "hour": 19,
+                    "minute": 18,
+                    "second": 24,
+                    "microsecond": 830000
+                },
+                "MasterUsername": "admintest",
+                "EngineVersion": "3.6.0",
+                "LicenseModel": "docdb",
+                "SnapshotType": "manual",
+                "PercentProgress": 100,
+                "StorageEncrypted": true,
+                "KmsKeyId": "arn:aws:kms:us-east-1:644160558196:key/b10f842a-feb7-4318-92d5-0640a75b7688",
+                "DBClusterSnapshotArn": "arn:aws:rds:us-east-1:644160558196:cluster-snapshot:test-cluster-final-snapshot",
+                "IAMDatabaseAuthenticationEnabled": false
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
diff --git a/tests/data/placebo/test_rds_cluster_snapshot_cross_account/tagging.GetResources_1.json b/tests/data/placebo/test_rds_cluster_snapshot_cross_account/tagging.GetResources_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rds_cluster_snapshot_cross_account/tagging.GetResources_1.json
@@ -0,0 +1,9 @@
+{
+    "status_code": 200,
+    "data": {
+        "PaginationToken": "",
+        "ResourceTagMappingList": [
+        ],
+        "ResponseMetadata": {}
+    }
+}
diff --git a/tests/data/placebo/test_rds_cluster_snapshot_get_resources/rds.DescribeDBClusterSnapshots_1.json b/tests/data/placebo/test_rds_cluster_snapshot_get_resources/rds.DescribeDBClusterSnapshots_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rds_cluster_snapshot_get_resources/rds.DescribeDBClusterSnapshots_1.json
@@ -0,0 +1,97 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBClusterSnapshots": [
+            {
+                "AvailabilityZones": [
+                    "us-east-1a",
+                    "us-east-1b",
+                    "us-east-1d"
+                ],
+                "DBClusterSnapshotIdentifier": "rds:database-1-2020-04-27-05-58",
+                "DBClusterIdentifier": "database-1",
+                "SnapshotCreateTime": {
+                    "__class__": "datetime",
+                    "year": 2020,
+                    "month": 4,
+                    "day": 27,
+                    "hour": 5,
+                    "minute": 58,
+                    "second": 55,
+                    "microsecond": 964000
+                },
+                "Engine": "aurora-postgresql",
+                "AllocatedStorage": 0,
+                "Status": "available",
+                "Port": 0,
+                "VpcId": "vpc-d2d616b5",
+                "ClusterCreateTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 10,
+                    "day": 20,
+                    "hour": 10,
+                    "minute": 53,
+                    "second": 28,
+                    "microsecond": 740000
+                },
+                "MasterUsername": "postgres",
+                "EngineVersion": "10.serverless_7",
+                "LicenseModel": "postgresql-license",
+                "SnapshotType": "automated",
+                "PercentProgress": 100,
+                "StorageEncrypted": true,
+                "KmsKeyId": "arn:aws:kms:us-east-1:644160558196:key/b10f842a-feb7-4318-92d5-0640a75b7688",
+                "DBClusterSnapshotArn": "arn:aws:rds:us-east-1:644160558196:cluster-snapshot:rds:database-1-2020-04-27-05-58",
+                "IAMDatabaseAuthenticationEnabled": false
+            },
+            {
+                "AvailabilityZones": [
+                    "us-east-1a",
+                    "us-east-1b",
+                    "us-east-1c",
+                    "us-east-1d",
+                    "us-east-1e",
+                    "us-east-1f"
+                ],
+                "DBClusterSnapshotIdentifier": "test-cluster-final-snapshot",
+                "DBClusterIdentifier": "test-cluster",
+                "SnapshotCreateTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 2,
+                    "day": 19,
+                    "hour": 15,
+                    "minute": 37,
+                    "second": 8,
+                    "microsecond": 707000
+                },
+                "Engine": "docdb",
+                "AllocatedStorage": 0,
+                "Status": "available",
+                "Port": 0,
+                "VpcId": "vpc-d2d616b5",
+                "ClusterCreateTime": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 1,
+                    "day": 15,
+                    "hour": 19,
+                    "minute": 18,
+                    "second": 24,
+                    "microsecond": 830000
+                },
+                "MasterUsername": "admintest",
+                "EngineVersion": "3.6.0",
+                "LicenseModel": "docdb",
+                "SnapshotType": "manual",
+                "PercentProgress": 100,
+                "StorageEncrypted": true,
+                "KmsKeyId": "arn:aws:kms:us-east-1:644160558196:key/b10f842a-feb7-4318-92d5-0640a75b7688",
+                "DBClusterSnapshotArn": "arn:aws:rds:us-east-1:644160558196:cluster-snapshot:test-cluster-final-snapshot",
+                "IAMDatabaseAuthenticationEnabled": false
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rds_cluster_snapshot_get_resources/tagging.GetResources_1.json b/tests/data/placebo/test_rds_cluster_snapshot_get_resources/tagging.GetResources_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rds_cluster_snapshot_get_resources/tagging.GetResources_1.json
@@ -0,0 +1,81 @@
+{
+    "status_code": 200,
+    "data": {
+        "PaginationToken": "",
+        "ResourceTagMappingList": [
+            {
+                "ResourceARN": "arn:aws:rds:us-east-1:644160558196:cluster-snapshot:rds:database-1-2020-04-21-05-58",
+                "Tags": [
+                    {
+                        "Key": "Owner",
+                        "Value": "kapil"
+                    }
+                ]
+            },
+            {
+                "ResourceARN": "arn:aws:rds:us-east-1:644160558196:cluster-snapshot:rds:database-1-2020-04-23-05-59",
+                "Tags": [
+                    {
+                        "Key": "Owner",
+                        "Value": "kapil"
+                    }
+                ]
+            },
+            {
+                "ResourceARN": "arn:aws:rds:us-east-1:644160558196:cluster-snapshot:rds:database-1-2020-04-22-05-58",
+                "Tags": [
+                    {
+                        "Key": "Owner",
+                        "Value": "kapil"
+                    }
+                ]
+            },
+            {
+                "ResourceARN": "arn:aws:rds:us-east-1:644160558196:cluster-snapshot:rds:database-1-2020-04-27-05-58",
+                "Tags": [
+                    {
+                        "Key": "Owner",
+                        "Value": "kapil"
+                    }
+                ]
+            },
+            {
+                "ResourceARN": "arn:aws:rds:us-east-1:644160558196:cluster-snapshot:rds:database-1-2020-04-26-05-58",
+                "Tags": [
+                    {
+                        "Key": "Owner",
+                        "Value": "kapil"
+                    }
+                ]
+            },
+            {
+                "ResourceARN": "arn:aws:rds:us-east-1:644160558196:cluster-snapshot:verify",
+                "Tags": [
+                    {
+                        "Key": "Owner",
+                        "Value": "kapil"
+                    }
+                ]
+            },
+            {
+                "ResourceARN": "arn:aws:rds:us-east-1:644160558196:cluster-snapshot:rds:database-1-2020-04-25-05-58",
+                "Tags": [
+                    {
+                        "Key": "Owner",
+                        "Value": "kapil"
+                    }
+                ]
+            },
+            {
+                "ResourceARN": "arn:aws:rds:us-east-1:644160558196:cluster-snapshot:rds:database-1-2020-04-24-05-58",
+                "Tags": [
+                    {
+                        "Key": "Owner",
+                        "Value": "kapil"
+                    }
+                ]
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/test_rdscluster.py b/tests/test_rdscluster.py
--- a/tests/test_rdscluster.py
+++ b/tests/test_rdscluster.py
@@ -380,6 +380,41 @@ def test_rdscluster_snapshot_simple(self):
         resources = p.run()
         self.assertEqual(len(resources), 2)
 
+    def test_rdscluster_snapshot_get_resources(self):
+        session_factory = self.replay_flight_data('test_rds_cluster_snapshot_get_resources')
+        p = self.load_policy(
+            {
+                'name': 'rdscluster-get',
+                'resource': 'aws.rds-cluster-snapshot'
+            },
+            session_factory=session_factory)
+        resources = p.resource_manager.get_resources([
+            'test-cluster-final-snapshot',
+            'invalid',
+            'rds:database-1-2020-04-27-05-58'])
+        self.assertEqual(len(resources), 2)
+        self.assertEqual(
+            {'rds:database-1-2020-04-27-05-58', 'test-cluster-final-snapshot'},
+            {r['DBClusterSnapshotIdentifier'] for r in resources})
+        self.assertEqual(
+            {len(r['Tags']) for r in resources},
+            {1, 0})
+
+    def test_rdscluster_snapshot_cross_account(self):
+        session_factory = self.replay_flight_data('test_rds_cluster_snapshot_cross_account')
+        p = self.load_policy(
+            {
+                'name': 'rdscluster-snapshot-xaccount',
+                'resource': 'aws.rds-cluster-snapshot',
+                'filters': [
+                    {'type': 'cross-account'}]
+            },
+            session_factory=session_factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        self.assertEqual(resources[0]['DBClusterSnapshotIdentifier'], 'test-cluster-final-snapshot')
+        self.assertEqual(resources[0]['c7n:CrossAccountViolations'], ['12345678910'])
+
     def test_rdscluster_snapshot_simple_filter(self):
         session_factory = self.replay_flight_data("test_rdscluster_snapshot_simple")
         p = self.load_policy(