1# Copyright The Cloud Custodian Authors.
2# SPDX-License-Identifier: Apache-2.0
3import json
4from botocore.exceptions import ClientError
5
6from c7n.actions import Action, BaseAction, RemovePolicyBase
7from c7n.exceptions import PolicyValidationError
8from c7n.filters.kms import KmsRelatedFilter
9from c7n.filters import Filter, CrossAccountAccessFilter
10from c7n.manager import resources
11from c7n.filters.vpc import SecurityGroupFilter, SubnetFilter, NetworkLocation
12from c7n.filters.policystatement import HasStatementFilter
13from c7n.query import (
14 QueryResourceManager, ChildResourceManager, TypeInfo, DescribeSource, ConfigSource
15)
16from c7n.tags import universal_augment
17from c7n.utils import local_session, type_schema, get_retry
18from .aws import shape_validate
19from c7n.filters.backup import ConsecutiveAwsBackupsFilter
20
21
22class EFSDescribe(DescribeSource):
23
24 def augment(self, resources):
25 return universal_augment(self.manager, resources)
26
27
28@resources.register('efs')
29class ElasticFileSystem(QueryResourceManager):
30
31 class resource_type(TypeInfo):
32 service = 'efs'
33 enum_spec = ('describe_file_systems', 'FileSystems', None)
34 id = 'FileSystemId'
35 name = 'Name'
36 date = 'CreationTime'
37 dimension = 'FileSystemId'
38 arn_type = 'file-system'
39 permission_prefix = arn_service = 'elasticfilesystem'
40 filter_name = 'FileSystemId'
41 filter_type = 'scalar'
42 universal_taggable = True
43 config_type = cfn_type = 'AWS::EFS::FileSystem'
44 arn = 'FileSystemArn'
45 permissions_augment = ("elasticfilesystem:ListTagsForResource",)
46
47 source_mapping = {
48 'describe': EFSDescribe,
49 'config': ConfigSource
50 }
51
52
53@resources.register('efs-mount-target')
54class ElasticFileSystemMountTarget(ChildResourceManager):
55
56 class resource_type(TypeInfo):
57 service = 'efs'
58 parent_spec = ('efs', 'FileSystemId', None)
59 enum_spec = ('describe_mount_targets', 'MountTargets', None)
60 permission_prefix = 'elasticfilesystem'
61 name = id = 'MountTargetId'
62 arn = False
63 cfn_type = 'AWS::EFS::MountTarget'
64 supports_trailevents = True
65
66
67@ElasticFileSystemMountTarget.filter_registry.register('subnet')
68class Subnet(SubnetFilter):
69
70 RelatedIdsExpression = "SubnetId"
71
72
73@ElasticFileSystemMountTarget.filter_registry.register('security-group')
74class SecurityGroup(SecurityGroupFilter):
75
76 efs_group_cache = None
77
78 RelatedIdsExpression = ""
79
80 def get_related_ids(self, resources):
81
82 if self.efs_group_cache:
83 group_ids = set()
84 for r in resources:
85 group_ids.update(
86 self.efs_group_cache.get(r['MountTargetId'], ()))
87 return list(group_ids)
88
89 client = local_session(self.manager.session_factory).client('efs')
90 groups = {}
91 group_ids = set()
92 retry = get_retry(('Throttled',), 12)
93
94 for r in resources:
95 groups[r['MountTargetId']] = retry(
96 client.describe_mount_target_security_groups,
97 MountTargetId=r['MountTargetId'])['SecurityGroups']
98 group_ids.update(groups[r['MountTargetId']])
99
100 self.efs_group_cache = groups
101 return list(group_ids)
102
103
104@ElasticFileSystemMountTarget.filter_registry.register('network-location', NetworkLocation)
105@ElasticFileSystem.filter_registry.register('kms-key')
106class KmsFilter(KmsRelatedFilter):
107
108 RelatedIdsExpression = 'KmsKeyId'
109
110
111@ElasticFileSystem.action_registry.register('delete')
112class Delete(Action):
113
114 schema = type_schema('delete')
115 permissions = ('elasticfilesystem:DescribeMountTargets',
116 'elasticfilesystem:DeleteMountTarget',
117 'elasticfilesystem:DeleteFileSystem')
118
119 def process(self, resources):
120 client = local_session(self.manager.session_factory).client('efs')
121 self.unmount_filesystems(resources)
122 retry = get_retry(('FileSystemInUse',), 12)
123 for r in resources:
124 retry(client.delete_file_system, FileSystemId=r['FileSystemId'])
125
126 def unmount_filesystems(self, resources):
127 client = local_session(self.manager.session_factory).client('efs')
128 for r in resources:
129 if not r['NumberOfMountTargets']:
130 continue
131 for t in client.describe_mount_targets(
132 FileSystemId=r['FileSystemId'])['MountTargets']:
133 client.delete_mount_target(MountTargetId=t['MountTargetId'])
134
135
136@ElasticFileSystem.action_registry.register('configure-lifecycle-policy')
137class ConfigureLifecycle(BaseAction):
138 """Enable/disable lifecycle policy for efs.
139
140 :example:
141
142 .. code-block:: yaml
143
144 policies:
145 - name: efs-apply-lifecycle
146 resource: efs
147 actions:
148 - type: configure-lifecycle-policy
149 state: enable
150 rules:
151 - 'TransitionToIA': 'AFTER_7_DAYS'
152
153 """
154 schema = type_schema(
155 'configure-lifecycle-policy',
156 state={'enum': ['enable', 'disable']},
157 rules={
158 'type': 'array',
159 'items': {'type': 'object'}},
160 required=['state'])
161
162 permissions = ('elasticfilesystem:PutLifecycleConfiguration',)
163 shape = 'PutLifecycleConfigurationRequest'
164
165 def validate(self):
166 if self.data.get('state') == 'enable' and 'rules' not in self.data:
167 raise PolicyValidationError(
168 'rules are required to enable lifecycle configuration %s' % (self.manager.data))
169 if self.data.get('state') == 'disable' and 'rules' in self.data:
170 raise PolicyValidationError(
171 'rules not required to disable lifecycle configuration %s' % (self.manager.data))
172 if self.data.get('rules'):
173 attrs = {}
174 attrs['LifecyclePolicies'] = self.data['rules']
175 attrs['FileSystemId'] = 'PolicyValidator'
176 return shape_validate(attrs, self.shape, 'efs')
177
178 def process(self, resources):
179 client = local_session(self.manager.session_factory).client('efs')
180 op_map = {'enable': self.data.get('rules'), 'disable': []}
181 for r in resources:
182 try:
183 client.put_lifecycle_configuration(
184 FileSystemId=r['FileSystemId'],
185 LifecyclePolicies=op_map.get(self.data.get('state')))
186 except client.exceptions.FileSystemNotFound:
187 continue
188
189
190@ElasticFileSystem.filter_registry.register('lifecycle-policy')
191class LifecyclePolicy(Filter):
192 """Filters efs based on the state of lifecycle policies
193
194 :example:
195
196 .. code-block:: yaml
197
198 policies:
199 - name: efs-filter-lifecycle
200 resource: efs
201 filters:
202 - type: lifecycle-policy
203 state: present
204 value: AFTER_7_DAYS
205
206 """
207 schema = type_schema(
208 'lifecycle-policy',
209 state={'enum': ['present', 'absent']},
210 value={'type': 'string'},
211 required=['state'])
212
213 permissions = ('elasticfilesystem:DescribeLifecycleConfiguration',)
214
215 def process(self, resources, event=None):
216 resources = self.fetch_resources_lfc(resources)
217 if self.data.get('value'):
218 config = {'TransitionToIA': self.data.get('value')}
219 if self.data.get('state') == 'present':
220 return [r for r in resources if config in r.get('c7n:LifecyclePolicies')]
221 return [r for r in resources if config not in r.get('c7n:LifecyclePolicies')]
222 else:
223 if self.data.get('state') == 'present':
224 return [r for r in resources if r.get('c7n:LifecyclePolicies')]
225 return [r for r in resources if r.get('c7n:LifecyclePolicies') == []]
226
227 def fetch_resources_lfc(self, resources):
228 client = local_session(self.manager.session_factory).client('efs')
229 for r in resources:
230 try:
231 lfc = client.describe_lifecycle_configuration(
232 FileSystemId=r['FileSystemId']).get('LifecyclePolicies')
233 r['c7n:LifecyclePolicies'] = lfc
234 except client.exceptions.FileSystemNotFound:
235 continue
236 return resources
237
238
239@ElasticFileSystem.filter_registry.register('check-secure-transport')
240class CheckSecureTransport(Filter):
241 """Find EFS that does not enforce secure transport
242
243 :Example:
244
245 .. code-block:: yaml
246
247 - name: efs-securetransport-check-policy
248 resource: efs
249 filters:
250 - check-secure-transport
251
252 To configure an EFS to enforce secure transport, set up the appropriate
253 Effect and Condition for its policy document. For example:
254
255 .. code-block:: json
256
257 {
258 "Sid": "efs-statement-b3f6b59b-d938-4001-9154-508f67707073",
259 "Effect": "Deny",
260 "Principal": { "AWS": "*" },
261 "Action": "*",
262 "Condition": {
263 "Bool": { "aws:SecureTransport": "false" }
264 }
265 }
266 """
267
268 schema = type_schema('check-secure-transport')
269 permissions = ('elasticfilesystem:DescribeFileSystemPolicy',)
270
271 policy_annotation = 'c7n:Policy'
272
273 def get_policy(self, client, resource):
274 if self.policy_annotation in resource:
275 return resource[self.policy_annotation]
276 try:
277 result = client.describe_file_system_policy(
278 FileSystemId=resource['FileSystemId'])
279 except client.exceptions.PolicyNotFound:
280 return None
281 resource[self.policy_annotation] = json.loads(result['Policy'])
282 return resource[self.policy_annotation]
283
284 def securetransport_check_policy(self, client, resource):
285 policy = self.get_policy(client, resource)
286 if not policy:
287 return True
288
289 statements = policy['Statement']
290 if isinstance(statements, dict):
291 statements = [statements]
292
293 for s in statements:
294 try:
295 effect = s['Effect']
296 secureTransportValue = s['Condition']['Bool']['aws:SecureTransport']
297 if ((effect == 'Deny' and secureTransportValue == 'false') or
298 (effect == 'Allow' and secureTransportValue == 'true')):
299 return False
300 except (KeyError, TypeError):
301 pass
302
303 return True
304
305 def process(self, resources, event=None):
306 c = local_session(self.manager.session_factory).client('efs')
307 results = [r for r in resources if self.securetransport_check_policy(c, r)]
308 self.log.info(
309 "%d of %d EFS policies don't enforce secure transport",
310 len(results), len(resources))
311 return results
312
313
314@ElasticFileSystem.filter_registry.register('has-statement')
315class EFSHasStatementFilter(HasStatementFilter):
316
317 def __init__(self, data, manager=None):
318 super().__init__(data, manager)
319 self.policy_attribute = 'c7n:Policy'
320
321 def process(self, resources, event=None):
322 resources = [self.policy_annotate(r) for r in resources]
323 return super().process(resources, event)
324
325 def policy_annotate(self, resource):
326 client = local_session(self.manager.session_factory).client('efs')
327 if self.policy_attribute in resource:
328 return resource
329 try:
330 result = client.describe_file_system_policy(
331 FileSystemId=resource['FileSystemId'])
332 resource[self.policy_attribute] = result['Policy']
333 except client.exceptions.PolicyNotFound:
334 resource[self.policy_attribute] = None
335 return resource
336 return resource
337
338 def get_std_format_args(self, fs):
339 return {
340 'fs_arn': fs['FileSystemArn'],
341 'account_id': self.manager.config.account_id,
342 'region': self.manager.config.region
343 }
344
345
346@ElasticFileSystem.filter_registry.register('cross-account')
347class EFSCrossAccountFilter(CrossAccountAccessFilter):
348 """Filter EFS file systems which have cross account permissions
349
350 :example:
351
352 .. code-block:: yaml
353
354 policies:
355 - name: efs-cross-account
356 resource: aws.efs
357 filters:
358 - type: cross-account
359 """
360 permissions = ('elasticfilesystem:DescribeFileSystemPolicy',)
361
362 def process(self, resources, event=None):
363 def _augment(r):
364 client = local_session(
365 self.manager.session_factory).client('efs')
366 try:
367 r['Policy'] = client.describe_file_system_policy(
368 FileSystemId=r['FileSystemId'])['Policy']
369 return r
370 except ClientError as e:
371 if e.response['Error']['Code'] == 'AccessDeniedException':
372 self.log.warning(
373 "Access denied getting policy elasticfilesystems:%s",
374 r['FileSystemId'])
375
376 self.log.debug("fetching policy for %d elasticfilesystems" % len(resources))
377 with self.executor_factory(max_workers=3) as w:
378 resources = list(filter(None, w.map(_augment, resources)))
379
380 return super(EFSCrossAccountFilter, self).process(
381 resources, event)
382
383
384@ElasticFileSystem.action_registry.register('remove-statements')
385class RemovePolicyStatement(RemovePolicyBase):
386 """Action to remove policy statements from EFS
387
388 :example:
389
390 .. code-block:: yaml
391
392 policies:
393 - name: remove-efs-cross-account
394 resource: efs
395 filters:
396 - type: cross-account
397 actions:
398 - type: remove-statements
399 statement_ids: matched
400 """
401
402 schema = type_schema(
403 'remove-statements',
404 required=['statement_ids'],
405 statement_ids={'oneOf': [
406 {'enum': ['matched']},
407 {'type': 'array', 'items': {'type': 'string'}}]})
408
409 permissions = (
410 'elasticfilesystem:DescribeFileSystems', 'elasticfilesystem:DeleteFileSystemPolicy'
411 )
412
413 def process(self, resources):
414 results = []
415 client = local_session(self.manager.session_factory).client('efs')
416 for r in resources:
417 try:
418 results += filter(None, [self.process_resource(client, r)])
419 except Exception:
420 self.log.exception(
421 "Error processing elasticfilesystem:%s", r['FileSystemId'])
422 return results
423
424 def process_resource(self, client, resource):
425 if 'Policy' not in resource:
426 try:
427 resource['Policy'] = client.describe_file_system_policy(
428 FileSystemId=resource['FileSystemId']).get('Policy')
429 except ClientError as e:
430 if e.response['Error']['Code'] != "FileSystemNotFound":
431 raise
432
433 if not resource['Policy']:
434 return
435
436 p = json.loads(resource['Policy'])
437 statements, found = self.process_policy(
438 p, resource, CrossAccountAccessFilter.annotation_key)
439
440 if not found:
441 return
442
443 if not statements:
444 client.delete_file_system_policy(FileSystemId=resource['FileSystemId'])
445 else:
446 client.put_file_system_policy(
447 FileSystemId=resource['FileSystemId'],
448 Policy=json.dumps(p)
449 )
450 return {'Name': resource['FileSystemId'],
451 'State': 'PolicyRemoved',
452 'Statements': found}
453
454
455ElasticFileSystem.filter_registry.register('consecutive-aws-backups', ConsecutiveAwsBackupsFilter)