Repository: cloud-custodian/cloud-custodian

Issue:
AWS Lakeformation registered location for S3 and role
### Describe the feature

We're trying to come up with ways to prevent data exfiltration via AWS LakeFormation. As per my understanding, the service lets you register an Amazon S3 path for data lake. The goal is to make sure that bucket is not an external bucket. 

Proposed solution:

Since we do not have a way to detect ownership of the bucket, we're trying to lock down the buckets to a list of approved ones. AWS allows usage of `prefix/` in the bucket path for finer grained access control. 
Secondly, we're also trying to look at the IAM role used to register the bucket. Potentially creating custom roles and locking them to our org id as per this new iam feature release (https://aws.amazon.com/blogs/security/how-to-control-access-to-aws-resources-based-on-aws-account-ou-or-organization/)

We could potentially add a `datalake-registered-location` resource type. The api gives back the following details.
`>>> response = client.list_resources()
>>> response
{'ResponseMetadata': {'RequestId': '77fe1b0b-5c79-43f1-81e9-1d648dcf4eea', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Thu, 28 Jul 2022 22:37:34 GMT', 'content-type': 'application/json', 'content-length': '866', 'connection': 'keep-alive', 'x-amzn-requestid': '77fe1b0b-5c79-43f1-81e9-1d648dcf4eea', 'cache-control': 'no-cache'}, 'RetryAttempts': 0}, 'ResourceInfoList': [{'ResourceArn': 'arn:aws:s3:::test-1', 'RoleArn': 'arn:aws:iam::123456789:role/aws-service-role/lakeformation.amazonaws.com/AWSServiceRoleForLakeFormationDataAccess', 'LastModified': datetime.datetime(2022, 7, 26, 16, 58, 36, 160000, tzinfo=tzlocal())}, {'ResourceArn': 'arn:aws:s3:::test-4', 'RoleArn': 'arn:aws:iam::123456789:role/aws-service-role/lakeformation.amazonaws.com/AWSServiceRoleForLakeFormationDataAccess', 'LastModified': datetime.datetime(2022, 5, 3, 7, 56, 11, 572000, tzinfo=tzlocal())}, {'ResourceArn': 'arn:aws:s3:::test-2', 'RoleArn': 'arn:aws:iam::123456789:role/aws-service-role/lakeformation.amazonaws.com/AWSServiceRoleForLakeFormationDataAccess', 'LastModified': datetime.datetime(2022, 5, 3, 11, 4, 0, 568000, tzinfo=tzlocal())}, {'ResourceArn': 'arn:aws:s3:::test-3', 'RoleArn': 'arn:aws:iam::123456789:role/aws-service-role/lakeformation.amazonaws.com/AWSServiceRoleForLakeFormationDataAccess', 'LastModified': datetime.datetime(2022, 7, 26, 16, 57, 6, 196000, tzinfo=tzlocal())}]}`

A value filter to check the `RoleArn` and a filter to potentially look at S3 bucket name?

![image](https://user-images.githubusercontent.com/22061271/181652924-3c737db6-c6ef-44bd-93e9-c53600380d22.png)


### Extra information or context

_No response_


Discussion:
Hi @kapilt , we got a chance to discuss this with AWS too. Apparently, the only way to do it, is by locking the account via SCP such that the iam roles inside an account (data lake accounts for ex) do not have permissions to do read/write to a resource outside the org. 
Also, those SCP/IAM restrictions do not apply to AWS service linked roles. Would you be down to adding this as a resource type to look at the IAM role used to register the data lake location? Thank you!!
Realized that lakeformation doesn't support tagging. Might be worth adding it as a filter on `resource: account`

Resolving patch:
diff --git a/c7n/resources/lakeformation.py b/c7n/resources/lakeformation.py
new file mode 100644
--- /dev/null
+++ b/c7n/resources/lakeformation.py
@@ -0,0 +1,67 @@
+from c7n.manager import resources
+from c7n.query import QueryResourceManager, TypeInfo
+from c7n.actions import BaseAction
+from c7n.utils import local_session, type_schema
+from .aws import Arn
+from c7n.filters import Filter, ValueFilter
+
+
+@resources.register('datalake-location')
+class LakeFormationRegisteredLocation(QueryResourceManager):
+
+    class resource_type(TypeInfo):
+        service = 'lakeformation'
+        enum_spec = ('list_resources', 'ResourceInfoList', None)
+        name = arn = id = 'ResourceArn'
+        cfn_type = "AWS::LakeFormation::Resource"
+        arn_type = ''
+
+
+@LakeFormationRegisteredLocation.action_registry.register('deregister')
+class DeleteRegisteredLocation(BaseAction):
+    schema = type_schema('deregister')
+    permissions = ('lakeformation:DeregisterResource',)
+
+    def process(self, resources):
+        client = local_session(self.manager.session_factory).client('lakeformation')
+        for r in resources:
+            try:
+                self.manager.retry(client.deregister_resource, ResourceArn=r['ResourceArn'])
+            except client.exceptions.InvalidInputException:
+                continue
+
+
+@LakeFormationRegisteredLocation.filter_registry.register('cross-account')
+class DataLakeLocationsCrossAccount(Filter):
+    """Flags all registered datalake locations if it's cross account.
+
+    :example:
+
+    .. code-block:: yaml
+
+       policies:
+         - name: lakeformation-cross-account-location
+           resource: aws.datalake-location
+           filters:
+            - type: cross-account
+
+    """
+
+    schema = type_schema('cross-account', rinherit=ValueFilter.schema)
+    schema_alias = False
+    permissions = ('lakeformation:ListResources',)
+
+    def process(self, resources, event=None):
+        results = []
+        for r in resources:
+            if self.process_account(r):
+                results.append(r)
+        return results
+
+    def process_account(self, r):
+        lake_bucket = {Arn.parse(r.get('ResourceArn')).resource}
+        buckets = {
+            b['Name'] for b in
+            self.manager.get_resource_manager('s3').resources(augment=False)}
+        cross_account = lake_bucket.difference(buckets)
+        return cross_account
diff --git a/c7n/resources/resource_map.py b/c7n/resources/resource_map.py
--- a/c7n/resources/resource_map.py
+++ b/c7n/resources/resource_map.py
@@ -124,6 +124,7 @@
     "aws.kinesis-video": "c7n.resources.kinesis.KinesisVideoStream",
     "aws.kms": "c7n.resources.kms.KeyAlias",
     "aws.kms-key": "c7n.resources.kms.Key",
+    "aws.datalake-location": "c7n.resources.lakeformation.LakeFormationRegisteredLocation",
     "aws.lambda": "c7n.resources.awslambda.AWSLambda",
     "aws.lambda-layer": "c7n.resources.awslambda.LambdaLayerVersion",
     "aws.launch-config": "c7n.resources.asg.LaunchConfig",


Test patch:
diff --git a/tests/data/placebo/test_datalake_cross_account_deregister/lakeformation.DeregisterResource_1.json b/tests/data/placebo/test_datalake_cross_account_deregister/lakeformation.DeregisterResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_datalake_cross_account_deregister/lakeformation.DeregisterResource_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_datalake_cross_account_deregister/lakeformation.ListResources_1.json b/tests/data/placebo/test_datalake_cross_account_deregister/lakeformation.ListResources_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_datalake_cross_account_deregister/lakeformation.ListResources_1.json
@@ -0,0 +1,36 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "ResourceInfoList": [
+            {
+                "ResourceArn": "arn:aws:s3:::c7n-test-abc",
+                "RoleArn": "arn:aws:iam::644160558196:role/aws-service-role/lakeformation.amazonaws.com/AWSServiceRoleForLakeFormationDataAccess",
+                "LastModified": {
+                    "__class__": "datetime",
+                    "year": 2022,
+                    "month": 8,
+                    "day": 18,
+                    "hour": 14,
+                    "minute": 35,
+                    "second": 41,
+                    "microsecond": 967000
+                }
+            },
+            {
+                "ResourceArn": "arn:aws:s3:::unknown-bucket",
+                "RoleArn": "arn:aws:iam::644160558196:role/aws-service-role/lakeformation.amazonaws.com/AWSServiceRoleForLakeFormationDataAccess",
+                "LastModified": {
+                    "__class__": "datetime",
+                    "year": 2022,
+                    "month": 8,
+                    "day": 22,
+                    "hour": 15,
+                    "minute": 16,
+                    "second": 12,
+                    "microsecond": 811000
+                }
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_datalake_cross_account_deregister/lakeformation.ListResources_2.json b/tests/data/placebo/test_datalake_cross_account_deregister/lakeformation.ListResources_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_datalake_cross_account_deregister/lakeformation.ListResources_2.json
@@ -0,0 +1,22 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "ResourceInfoList": [
+            {
+                "ResourceArn": "arn:aws:s3:::c7n-test-abc",
+                "RoleArn": "arn:aws:iam::644160558196:role/aws-service-role/lakeformation.amazonaws.com/AWSServiceRoleForLakeFormationDataAccess",
+                "LastModified": {
+                    "__class__": "datetime",
+                    "year": 2022,
+                    "month": 8,
+                    "day": 18,
+                    "hour": 14,
+                    "minute": 35,
+                    "second": 41,
+                    "microsecond": 967000
+                }
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_datalake_cross_account_deregister/s3.ListBuckets_1.json b/tests/data/placebo/test_datalake_cross_account_deregister/s3.ListBuckets_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_datalake_cross_account_deregister/s3.ListBuckets_1.json
@@ -0,0 +1,38 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "Buckets": [
+            {
+                "Name": "c7n-test-abc",
+                "CreationDate": {
+                    "__class__": "datetime",
+                    "year": 2020,
+                    "month": 9,
+                    "day": 18,
+                    "hour": 20,
+                    "minute": 31,
+                    "second": 12,
+                    "microsecond": 0
+                }
+            },
+            {
+                "Name": "c7n-test-arn",
+                "CreationDate": {
+                    "__class__": "datetime",
+                    "year": 2020,
+                    "month": 12,
+                    "day": 15,
+                    "hour": 19,
+                    "minute": 13,
+                    "second": 28,
+                    "microsecond": 0
+                }
+            }
+        ],
+        "Owner": {
+            "DisplayName": "mandeep.bal",
+            "ID": "e7c8bb65a5fc49cf906715eae09de9e4bb7861a96361ba79b833aa45f6833b15"
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/test_lakeformation.py b/tests/test_lakeformation.py
new file mode 100644
--- /dev/null
+++ b/tests/test_lakeformation.py
@@ -0,0 +1,20 @@
+from .common import BaseTest
+
+
+class DataLakeRegisteredLocation(BaseTest):
+
+    def test_datalake_cross_account_deregister(self):
+        factory = self.replay_flight_data('test_datalake_cross_account_deregister')
+        p = self.load_policy({
+            'name': 'datalake-location-cross-account',
+            'resource': 'datalake-location',
+            'filters': [{'type': 'cross-account'}],
+            'actions': [{'type': 'deregister'}]},
+            session_factory=factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        self.assertEqual(resources[0]['ResourceArn'], 'arn:aws:s3:::unknown-bucket')
+        client = factory().client("lakeformation")
+        reg_loc = client.list_resources()['ResourceInfoList']
+        self.assertEqual(len(reg_loc), 1)
+        self.assertNotEqual((r.get('ResourceArn') for r in reg_loc), 'arn:aws:s3:::pratyush-123')