1# Copyright The Cloud Custodian Authors.
2# SPDX-License-Identifier: Apache-2.0
3import json
4from urllib.parse import urlparse, parse_qs
5
6from botocore.exceptions import ClientError
7from botocore.paginate import Paginator
8from concurrent.futures import as_completed
9from datetime import timedelta, datetime
10
11from c7n.actions import Action, RemovePolicyBase, ModifyVpcSecurityGroupsAction
12from c7n.filters import CrossAccountAccessFilter, ValueFilter, Filter
13from c7n.filters.costhub import CostHubRecommendation
14from c7n.filters.kms import KmsRelatedFilter
15import c7n.filters.vpc as net_filters
16from c7n.manager import resources
17from c7n import query, utils
18from c7n.resources.aws import shape_validate
19from c7n.resources.iam import CheckPermissions, SpecificIamRoleManagedPolicy
20from c7n.tags import universal_augment
21from c7n.utils import (
22 local_session,
23 type_schema,
24 select_keys,
25 get_human_size,
26 parse_date,
27 get_retry,
28 jmespath_search,
29 jmespath_compile
30)
31from botocore.config import Config
32from .securityhub import PostFinding
33
34ErrAccessDenied = "AccessDeniedException"
35
36
37class DescribeLambda(query.DescribeSource):
38
39 def augment(self, resources):
40 return universal_augment(
41 self.manager, super(DescribeLambda, self).augment(resources))
42
43 def get_resources(self, ids):
44 client = local_session(self.manager.session_factory).client('lambda')
45 resources = []
46 for rid in ids:
47 try:
48 func = self.manager.retry(client.get_function, FunctionName=rid)
49 except client.exceptions.ResourceNotFoundException:
50 continue
51 config = func.pop('Configuration')
52 config.update(func)
53 if 'Tags' in config:
54 config['Tags'] = [
55 {'Key': k, 'Value': v} for k, v in config['Tags'].items()]
56 resources.append(config)
57 return resources
58
59
60class ConfigLambda(query.ConfigSource):
61
62 def load_resource(self, item):
63 resource = super(ConfigLambda, self).load_resource(item)
64 resource['c7n:Policy'] = item[
65 'supplementaryConfiguration'].get('Policy')
66 return resource
67
68
69@resources.register('lambda')
70class AWSLambda(query.QueryResourceManager):
71
72 class resource_type(query.TypeInfo):
73 service = 'lambda'
74 arn = 'FunctionArn'
75 arn_type = 'function'
76 arn_separator = ":"
77 enum_spec = ('list_functions', 'Functions', None)
78 name = id = 'FunctionName'
79 date = 'LastModified'
80 dimension = 'FunctionName'
81 config_type = 'AWS::Lambda::Function'
82 cfn_type = 'AWS::Lambda::Function'
83 universal_taggable = object()
84 permissions_augment = ("lambda:ListTags",)
85
86 source_mapping = {
87 'describe': DescribeLambda,
88 'config': ConfigLambda
89 }
90
91 def get_resources(self, ids, cache=True, augment=False):
92 return super(AWSLambda, self).get_resources(ids, cache, augment)
93
94
95@AWSLambda.filter_registry.register('security-group')
96class SecurityGroupFilter(net_filters.SecurityGroupFilter):
97
98 RelatedIdsExpression = "VpcConfig.SecurityGroupIds[]"
99
100
101@AWSLambda.filter_registry.register('subnet')
102class SubnetFilter(net_filters.SubnetFilter):
103
104 RelatedIdsExpression = "VpcConfig.SubnetIds[]"
105
106
107@AWSLambda.filter_registry.register('vpc')
108class VpcFilter(net_filters.VpcFilter):
109
110 RelatedIdsExpression = "VpcConfig.VpcId"
111
112
113AWSLambda.filter_registry.register('network-location', net_filters.NetworkLocation)
114
115
116@AWSLambda.filter_registry.register('check-permissions')
117class LambdaPermissions(CheckPermissions):
118
119 def get_iam_arns(self, resources):
120 return [r['Role'] for r in resources]
121
122
123@AWSLambda.filter_registry.register('url-config')
124class URLConfig(ValueFilter):
125
126 annotation_key = "c7n:UrlConfig"
127 schema = type_schema('url-config', rinherit=ValueFilter.schema)
128 schema_alias = False
129 permissions = ('lambda:GetFunctionUrlConfig',)
130
131 def process(self, resources, event=None):
132 client = local_session(self.manager.session_factory).client('lambda')
133
134 def _augment(r):
135 try:
136 r[self.annotation_key] = self.manager.retry(
137 client.get_function_url_config, FunctionName=r['FunctionArn'])
138 r[self.annotation_key].pop('ResponseMetadata')
139 except client.exceptions.ResourceNotFoundException:
140 r[self.annotation_key] = {}
141 return r
142
143 with self.executor_factory(max_workers=2) as w:
144 resources = list(filter(None, w.map(_augment, resources)))
145
146 return super().process(resources, event)
147
148 def __call__(self, i):
149 return super().__call__(i[self.annotation_key])
150
151
152@AWSLambda.filter_registry.register('reserved-concurrency')
153class ReservedConcurrency(ValueFilter):
154
155 annotation_key = "c7n:FunctionInfo"
156 value_key = '"c7n:FunctionInfo".Concurrency.ReservedConcurrentExecutions'
157 schema = type_schema('reserved-concurrency', rinherit=ValueFilter.schema)
158 schema_alias = False
159 permissions = ('lambda:GetFunction',)
160
161 def validate(self):
162 self.data['key'] = self.value_key
163 return super(ReservedConcurrency, self).validate()
164
165 def process(self, resources, event=None):
166 self.data['key'] = self.value_key
167 client = local_session(self.manager.session_factory).client('lambda')
168
169 def _augment(r):
170 try:
171 r[self.annotation_key] = self.manager.retry(
172 client.get_function, FunctionName=r['FunctionArn'])
173 r[self.annotation_key].pop('ResponseMetadata')
174 except ClientError as e:
175 if e.response['Error']['Code'] == ErrAccessDenied:
176 self.log.warning(
177 "Access denied getting lambda:%s",
178 r['FunctionName'])
179 raise
180 return r
181
182 with self.executor_factory(max_workers=3) as w:
183 resources = list(filter(None, w.map(_augment, resources)))
184 return super(ReservedConcurrency, self).process(resources, event)
185
186
187def get_lambda_policies(client, executor_factory, resources, log):
188
189 def _augment(r):
190 try:
191 r['c7n:Policy'] = client.get_policy(
192 FunctionName=r['FunctionName'])['Policy']
193 except client.exceptions.ResourceNotFoundException:
194 return None
195 except ClientError as e:
196 if e.response['Error']['Code'] == 'AccessDeniedException':
197 log.warning(
198 "Access denied getting policy lambda:%s",
199 r['FunctionName'])
200 return r
201
202 results = []
203 futures = {}
204
205 with executor_factory(max_workers=3) as w:
206 for r in resources:
207 if 'c7n:Policy' in r:
208 results.append(r)
209 continue
210 futures[w.submit(_augment, r)] = r
211
212 for f in as_completed(futures):
213 if f.exception():
214 log.warning("Error getting policy for:%s err:%s",
215 r['FunctionName'], f.exception())
216 r = futures[f]
217 continue
218 results.append(f.result())
219
220 return filter(None, results)
221
222
223@AWSLambda.filter_registry.register('event-source')
224class LambdaEventSource(ValueFilter):
225 # this uses iam policy, it should probably use
226 # event source mapping api
227
228 annotation_key = "c7n:EventSources"
229 schema = type_schema('event-source', rinherit=ValueFilter.schema)
230 schema_alias = False
231 permissions = ('lambda:GetPolicy',)
232
233 def process(self, resources, event=None):
234 client = local_session(self.manager.session_factory).client('lambda')
235 self.log.debug("fetching policy for %d lambdas" % len(resources))
236 resources = get_lambda_policies(
237 client, self.executor_factory, resources, self.log)
238 self.data['key'] = self.annotation_key
239 return super(LambdaEventSource, self).process(resources, event)
240
241 def __call__(self, r):
242 if 'c7n:Policy' not in r:
243 return False
244 sources = set()
245 data = json.loads(r['c7n:Policy'])
246 for s in data.get('Statement', ()):
247 if s['Effect'] != 'Allow':
248 continue
249 if 'Service' in s['Principal']:
250 sources.add(s['Principal']['Service'])
251 if sources:
252 r[self.annotation_key] = list(sources)
253 return self.match(r)
254
255
256@AWSLambda.filter_registry.register('cross-account')
257class LambdaCrossAccountAccessFilter(CrossAccountAccessFilter):
258 """Filters lambda functions with cross-account permissions
259
260 The whitelist parameter can be used to prevent certain accounts
261 from being included in the results (essentially stating that these
262 accounts permissions are allowed to exist)
263
264 This can be useful when combining this filter with the delete action.
265
266 :example:
267
268 .. code-block:: yaml
269
270 policies:
271 - name: lambda-cross-account
272 resource: lambda
273 filters:
274 - type: cross-account
275 whitelist:
276 - 'IAM-Policy-Cross-Account-Access'
277
278 """
279 permissions = ('lambda:GetPolicy',)
280
281 policy_attribute = 'c7n:Policy'
282
283 def process(self, resources, event=None):
284 client = local_session(self.manager.session_factory).client('lambda')
285 self.log.debug("fetching policy for %d lambdas" % len(resources))
286 resources = get_lambda_policies(
287 client, self.executor_factory, resources, self.log)
288 return super(LambdaCrossAccountAccessFilter, self).process(
289 resources, event)
290
291
292@AWSLambda.filter_registry.register('kms-key')
293class KmsFilter(KmsRelatedFilter):
294
295 RelatedIdsExpression = 'KMSKeyArn'
296
297
298@AWSLambda.filter_registry.register('has-specific-managed-policy')
299class HasSpecificManagedPolicy(SpecificIamRoleManagedPolicy):
300 """Filter an lambda function that has an IAM execution role that has a
301 specific managed IAM policy.
302
303 :example:
304
305 .. code-block:: yaml
306
307 policies:
308 - name: lambda-has-admin-policy
309 resource: aws.lambda
310 filters:
311 - type: has-specific-managed-policy
312 value: admin-policy
313
314 """
315
316 permissions = ('iam:ListAttachedRolePolicies',)
317
318 def process(self, resources, event=None):
319 client = utils.local_session(self.manager.session_factory).client('iam')
320
321 results = []
322 roles = {
323 r['Role']: {
324 'RoleName': r['Role'].split('/')[-1]
325 }
326 for r in resources
327 }
328
329 for role in roles.values():
330 self.get_managed_policies(client, [role])
331 for r in resources:
332 role_arn = r['Role']
333 matched_keys = [k for k in roles[role_arn][self.annotation_key] if self.match(k)]
334 self.merge_annotation(role, self.matched_annotation_key, matched_keys)
335 if matched_keys:
336 results.append(r)
337
338 return results
339
340
341@AWSLambda.action_registry.register('update')
342class UpdateLambda(Action):
343 """Update a lambda's configuration.
344
345 This action also has specific support for enacting recommendations
346 from the AWS Cost Optimization Hub for resizing.
347
348 :example:
349
350 .. code-block:: yaml
351
352 policies:
353 - name: lambda-rightsize
354 resource: aws.lambda
355 filters:
356 - type: cost-optimization
357 attrs:
358 - actionType: Rightsize
359 actions:
360 - update
361
362 """
363 schema = type_schema('update', properties={'type': 'object'})
364 permissions = ("lambda:UpdateFunctionConfiguration",)
365
366 def validate(self):
367 props = self.data.get('properties', {})
368 props['FunctionName'] = 'validation'
369 shape_validate(props, 'UpdateFunctionConfigurationRequest', 'lambda')
370
371 def process(self, resources):
372 client = utils.local_session(self.manager.session_factory).client('lambda')
373 retry = get_retry(('TooManyRequestsException', 'ResourceConflictException'))
374
375 for r in resources:
376 params = self.get_parameters(r)
377 params.pop('FunctionName', None)
378 try:
379 retry(
380 client.update_function_configuration,
381 FunctionName=r['FunctionName'],
382 **params
383 )
384 except client.exceptions.ResourceNotFoundException:
385 continue
386
387 def get_parameters(self, r):
388 params = self.data.get('properties', {})
389 hub_recommendation = r.get(CostHubRecommendation.annotation_key)
390 if hub_recommendation and hub_recommendation['actionType'] == 'Rightsize':
391 size = int(hub_recommendation['recommendedResourceSummary'].split(' ')[0])
392 params['MemorySize'] = size
393 return params
394
395
396@AWSLambda.action_registry.register('set-xray-tracing')
397class LambdaEnableXrayTracing(Action):
398 """
399 This action allows for enable Xray tracing to Active
400
401 :example:
402
403 .. code-block:: yaml
404
405 actions:
406 - type: enable-xray-tracing
407 """
408
409 schema = type_schema(
410 'set-xray-tracing',
411 **{'state': {'default': True, 'type': 'boolean'}}
412 )
413 permissions = ("lambda:UpdateFunctionConfiguration",)
414
415 def get_mode_val(self, state):
416 if state:
417 return "Active"
418 return "PassThrough"
419
420 def process(self, resources):
421 """
422 Enables the Xray Tracing for the function.
423
424 Args:
425 resources: AWS lamdba resources
426 Returns:
427 None
428 """
429 config = Config(
430 retries={
431 'max_attempts': 8,
432 'mode': 'standard'
433 }
434 )
435 client = local_session(self.manager.session_factory).client('lambda', config=config)
436 updateState = self.data.get('state', True)
437 retry = get_retry(('TooManyRequestsException', 'ResourceConflictException'))
438
439 mode = self.get_mode_val(updateState)
440 for resource in resources:
441 state = bool(resource["TracingConfig"]["Mode"] == "Active")
442 if updateState != state:
443 function_name = resource["FunctionName"]
444 self.log.info(f"Set Xray tracing to {mode} for lambda {function_name}")
445 try:
446 retry(
447 client.update_function_configuration,
448 FunctionName=function_name,
449 TracingConfig={
450 'Mode': mode
451 }
452 )
453 except client.exceptions.ResourceNotFoundException:
454 continue
455
456
457@AWSLambda.action_registry.register('post-finding')
458class LambdaPostFinding(PostFinding):
459
460 resource_type = 'AwsLambdaFunction'
461
462 def format_resource(self, r):
463 envelope, payload = self.format_envelope(r)
464 # security hub formatting beggars belief
465 details = self.filter_empty(select_keys(r,
466 ['CodeSha256',
467 'DeadLetterConfig',
468 'Environment',
469 'Handler',
470 'LastModified',
471 'MemorySize',
472 'MasterArn',
473 'RevisionId',
474 'Role',
475 'Runtime',
476 'TracingConfig',
477 'Timeout',
478 'Version',
479 'VpcConfig']))
480 # check and set the correct formatting value for kms key arn if it exists
481 kms_value = r.get('KMSKeyArn')
482 if kms_value is not None:
483 details['KmsKeyArn'] = kms_value
484 # do the brain dead parts Layers, Code, TracingConfig
485 if 'Layers' in r:
486 r['Layers'] = {
487 'Arn': r['Layers'][0]['Arn'],
488 'CodeSize': r['Layers'][0]['CodeSize']}
489 details.get('VpcConfig', {}).pop('VpcId', None)
490
491 if 'Code' in r and r['Code'].get('RepositoryType') == "S3":
492 parsed = urlparse(r['Code']['Location'])
493 details['Code'] = {
494 'S3Bucket': parsed.netloc.split('.', 1)[0],
495 'S3Key': parsed.path[1:]}
496 params = parse_qs(parsed.query)
497 if params['versionId']:
498 details['Code']['S3ObjectVersion'] = params['versionId'][0]
499 payload.update(details)
500 return envelope
501
502
503@AWSLambda.action_registry.register('trim-versions')
504class VersionTrim(Action):
505 """Delete old versions of a function.
506
507 By default this will only remove the non $LATEST
508 version of a function that are not referenced by
509 an alias. Optionally it can delete only versions
510 older than a given age.
511
512 :example:
513
514 .. code-block:: yaml
515
516 policies:
517 - name: lambda-gc
518 resource: aws.lambda
519 actions:
520 - type: trim-versions
521 exclude-aliases: true # default true
522 older-than: 60 # default not-set
523 retain-latest: true # default false
524
525 retain-latest refers to whether the latest numeric
526 version will be retained, the $LATEST alias will
527 still point to the last revision even without this set,
528 so this is safe wrt to the function availability, its more
529 about desire to retain an explicit version of the current
530 code, rather than just the $LATEST alias pointer which will
531 be automatically updated.
532 """
533 permissions = ('lambda:ListAliases', 'lambda:ListVersionsByFunction',
534 'lambda:DeleteFunction',)
535
536 schema = type_schema(
537 'trim-versions',
538 **{'exclude-aliases': {'default': True, 'type': 'boolean'},
539 'retain-latest': {'default': True, 'type': 'boolean'},
540 'older-than': {'type': 'number'}})
541
542 def process(self, resources):
543 client = local_session(self.manager.session_factory).client('lambda')
544 matched = total = 0
545 for r in resources:
546 fmatched, ftotal = self.process_lambda(client, r)
547 matched += fmatched
548 total += ftotal
549 self.log.info('trim-versions cleaned %s of %s lambda storage' % (
550 get_human_size(matched), get_human_size(total)))
551
552 def get_aliased_versions(self, client, r):
553 aliases_pager = client.get_paginator('list_aliases')
554 aliases_pager.PAGE_ITERATOR_CLASS = query.RetryPageIterator
555 aliases = aliases_pager.paginate(
556 FunctionName=r['FunctionName']).build_full_result().get('Aliases')
557
558 aliased_versions = set()
559 for a in aliases:
560 aliased_versions.add("%s:%s" % (
561 a['AliasArn'].rsplit(':', 1)[0], a['FunctionVersion']))
562 return aliased_versions
563
564 def process_lambda(self, client, r):
565 exclude_aliases = self.data.get('exclude-aliases', True)
566 retain_latest = self.data.get('retain-latest', False)
567 date_threshold = self.data.get('older-than')
568 date_threshold = (
569 date_threshold and
570 parse_date(datetime.utcnow()) - timedelta(days=date_threshold) or
571 None)
572 aliased_versions = ()
573
574 if exclude_aliases:
575 aliased_versions = self.get_aliased_versions(client, r)
576
577 versions_pager = client.get_paginator('list_versions_by_function')
578 versions_pager.PAGE_ITERATOR_CLASS = query.RetryPageIterator
579 pager = versions_pager.paginate(FunctionName=r['FunctionName'])
580
581 matched = total = 0
582 latest_sha = None
583
584 for page in pager:
585 versions = page.get('Versions')
586 for v in versions:
587 if v['Version'] == '$LATEST':
588 latest_sha = v['CodeSha256']
589 continue
590 total += v['CodeSize']
591 if v['FunctionArn'] in aliased_versions:
592 continue
593 if date_threshold and parse_date(v['LastModified']) > date_threshold:
594 continue
595 # Retain numbered version, not required, but it feels like a good thing
596 # to do. else the latest alias will still point.
597 if retain_latest and latest_sha and v['CodeSha256'] == latest_sha:
598 continue
599 matched += v['CodeSize']
600 self.manager.retry(
601 client.delete_function, FunctionName=v['FunctionArn'])
602 return (matched, total)
603
604
605@AWSLambda.action_registry.register('remove-statements')
606class RemovePolicyStatement(RemovePolicyBase):
607 """Action to remove policy/permission statements from lambda functions.
608
609 :example:
610
611 .. code-block:: yaml
612
613 policies:
614 - name: lambda-remove-cross-accounts
615 resource: lambda
616 filters:
617 - type: cross-account
618 actions:
619 - type: remove-statements
620 statement_ids: matched
621 """
622
623 schema = type_schema(
624 'remove-statements',
625 required=['statement_ids'],
626 statement_ids={'oneOf': [
627 {'enum': ['matched']},
628 {'type': 'array', 'items': {'type': 'string'}}]})
629
630 permissions = ("lambda:GetPolicy", "lambda:RemovePermission")
631
632 def process(self, resources):
633 results = []
634 client = local_session(self.manager.session_factory).client('lambda')
635 for r in resources:
636 try:
637 if self.process_resource(client, r):
638 results.append(r)
639 except Exception:
640 self.log.exception(
641 "Error processing lambda %s", r['FunctionArn'])
642 return results
643
644 def process_resource(self, client, resource):
645 if 'c7n:Policy' not in resource:
646 try:
647 resource['c7n:Policy'] = client.get_policy(
648 FunctionName=resource['FunctionName']).get('Policy')
649 except ClientError as e:
650 if e.response['Error']['Code'] != ErrAccessDenied:
651 raise
652 resource['c7n:Policy'] = None
653
654 if not resource['c7n:Policy']:
655 return
656
657 p = json.loads(resource['c7n:Policy'])
658
659 _, found = self.process_policy(
660 p, resource, CrossAccountAccessFilter.annotation_key)
661 if not found:
662 return
663
664 for f in found:
665 client.remove_permission(
666 FunctionName=resource['FunctionName'],
667 StatementId=f['Sid'])
668
669
670@AWSLambda.action_registry.register('set-concurrency')
671class SetConcurrency(Action):
672 """Set lambda function concurrency to the desired level.
673
674 Can be used to set the reserved function concurrency to an exact value,
675 to delete reserved concurrency, or to set the value to an attribute of
676 the resource.
677 """
678
679 schema = type_schema(
680 'set-concurrency',
681 required=('value',),
682 **{'expr': {'type': 'boolean'},
683 'value': {'oneOf': [
684 {'type': 'string'},
685 {'type': 'integer'},
686 {'type': 'null'}]}})
687
688 permissions = ('lambda:DeleteFunctionConcurrency',
689 'lambda:PutFunctionConcurrency')
690
691 def validate(self):
692 if self.data.get('expr', False) and not isinstance(self.data['value'], str):
693 raise ValueError("invalid value expression %s" % self.data['value'])
694 return self
695
696 def process(self, functions):
697 client = local_session(self.manager.session_factory).client('lambda')
698 is_expr = self.data.get('expr', False)
699 value = self.data['value']
700 if is_expr:
701 value = jmespath_compile(value)
702
703 none_type = type(None)
704
705 for function in functions:
706 fvalue = value
707 if is_expr:
708 fvalue = value.search(function)
709 if isinstance(fvalue, float):
710 fvalue = int(fvalue)
711 if isinstance(value, int) or isinstance(value, none_type):
712 self.policy.log.warning(
713 "Function: %s Invalid expression value for concurrency: %s",
714 function['FunctionName'], fvalue)
715 continue
716 if fvalue is None:
717 client.delete_function_concurrency(
718 FunctionName=function['FunctionName'])
719 else:
720 client.put_function_concurrency(
721 FunctionName=function['FunctionName'],
722 ReservedConcurrentExecutions=fvalue)
723
724
725@AWSLambda.action_registry.register('delete')
726class Delete(Action):
727 """Delete a lambda function (including aliases and older versions).
728
729 :example:
730
731 .. code-block:: yaml
732
733 policies:
734 - name: lambda-delete-dotnet-functions
735 resource: lambda
736 filters:
737 - Runtime: dotnetcore1.0
738 actions:
739 - delete
740 """
741 schema = type_schema('delete')
742 permissions = ("lambda:DeleteFunction",)
743
744 def process(self, functions):
745 client = local_session(self.manager.session_factory).client('lambda')
746 for function in functions:
747 try:
748 client.delete_function(FunctionName=function['FunctionName'])
749 except ClientError as e:
750 if e.response['Error']['Code'] == "ResourceNotFoundException":
751 continue
752 raise
753 self.log.debug("Deleted %d functions", len(functions))
754
755
756@AWSLambda.action_registry.register('modify-security-groups')
757class LambdaModifyVpcSecurityGroups(ModifyVpcSecurityGroupsAction):
758
759 permissions = ("lambda:UpdateFunctionConfiguration",)
760
761 def process(self, functions):
762 client = local_session(self.manager.session_factory).client('lambda')
763 groups = super(LambdaModifyVpcSecurityGroups, self).get_groups(
764 functions)
765
766 for idx, i in enumerate(functions):
767 if 'VpcConfig' not in i: # only continue if Lambda func is VPC-enabled
768 continue
769 try:
770 client.update_function_configuration(FunctionName=i['FunctionName'],
771 VpcConfig={'SecurityGroupIds': groups[idx]})
772 except client.exceptions.ResourceNotFoundException:
773 continue
774
775
776@resources.register('lambda-layer')
777class LambdaLayerVersion(query.QueryResourceManager):
778 """Note custodian models the lambda layer version.
779
780 Layers end up being a logical asset, the physical asset for use
781 and management is the layer verison.
782
783 To ease that distinction, we support querying just the latest
784 layer version or having a policy against all layer versions.
785
786 By default we query all versions, the following is an example
787 to query just the latest.
788
789 .. code-block:: yaml
790
791 policies:
792 - name: lambda-layer
793 resource: lambda
794 query:
795 - version: latest
796
797 """
798
799 class resource_type(query.TypeInfo):
800 service = 'lambda'
801 enum_spec = ('list_layers', 'Layers', None)
802 name = id = 'LayerName'
803 date = 'CreatedDate'
804 arn = "LayerVersionArn"
805 arn_type = "layer"
806 cfn_type = 'AWS::Lambda::LayerVersion'
807
808 def augment(self, resources):
809 versions = {}
810 for r in resources:
811 versions[r['LayerName']] = v = r['LatestMatchingVersion']
812 v['LayerName'] = r['LayerName']
813
814 if {'version': 'latest'} in self.data.get('query', []):
815 return list(versions.values())
816
817 layer_names = list(versions)
818 client = local_session(self.session_factory).client('lambda')
819
820 versions = []
821 for layer_name in layer_names:
822 pager = get_layer_version_paginator(client)
823 for v in pager.paginate(
824 LayerName=layer_name).build_full_result().get('LayerVersions'):
825 v['LayerName'] = layer_name
826 versions.append(v)
827 return versions
828
829
830def get_layer_version_paginator(client):
831 pager = Paginator(
832 client.list_layer_versions,
833 {'input_token': 'NextToken',
834 'output_token': 'NextToken',
835 'result_key': 'LayerVersions'},
836 client.meta.service_model.operation_model('ListLayerVersions'))
837 pager.PAGE_ITERATOR_CLS = query.RetryPageIterator
838 return pager
839
840
841@LambdaLayerVersion.filter_registry.register('cross-account')
842class LayerCrossAccount(CrossAccountAccessFilter):
843
844 permissions = ('lambda:GetLayerVersionPolicy',)
845
846 def process(self, resources, event=None):
847 client = local_session(self.manager.session_factory).client('lambda')
848 for r in resources:
849 if 'c7n:Policy' in r:
850 continue
851 try:
852 rpolicy = self.manager.retry(
853 client.get_layer_version_policy,
854 LayerName=r['LayerName'],
855 VersionNumber=r['Version']).get('Policy')
856 except client.exceptions.ResourceNotFoundException:
857 rpolicy = {}
858 r['c7n:Policy'] = rpolicy
859 return super(LayerCrossAccount, self).process(resources)
860
861 def get_resource_policy(self, r):
862 return r['c7n:Policy']
863
864
865@LambdaLayerVersion.action_registry.register('remove-statements')
866class LayerRemovePermissions(RemovePolicyBase):
867
868 schema = type_schema(
869 'remove-statements',
870 required=['statement_ids'],
871 statement_ids={'oneOf': [
872 {'enum': ['matched']},
873 {'type': 'array', 'items': {'type': 'string'}}]})
874
875 permissions = (
876 "lambda:GetLayerVersionPolicy",
877 "lambda:RemoveLayerVersionPermission")
878
879 def process(self, resources):
880 client = local_session(self.manager.session_factory).client('lambda')
881 for r in resources:
882 self.process_resource(client, r)
883
884 def process_resource(self, client, r):
885 if 'c7n:Policy' not in r:
886 try:
887 r['c7n:Policy'] = self.manager.retry(
888 client.get_layer_version_policy,
889 LayerName=r['LayerName'],
890 VersionNumber=r['Version'])
891 except client.exceptions.ResourceNotFound:
892 return
893
894 p = json.loads(r['c7n:Policy'])
895
896 _, found = self.process_policy(
897 p, r, CrossAccountAccessFilter.annotation_key)
898
899 if not found:
900 return
901
902 for f in found:
903 self.manager.retry(
904 client.remove_layer_version_permission,
905 LayerName=r['LayerName'],
906 StatementId=f['Sid'],
907 VersionNumber=r['Version'])
908
909
910@LambdaLayerVersion.action_registry.register('delete')
911class DeleteLayerVersion(Action):
912
913 schema = type_schema('delete')
914 permissions = ('lambda:DeleteLayerVersion',)
915
916 def process(self, resources):
917 client = local_session(
918 self.manager.session_factory).client('lambda')
919
920 for r in resources:
921 try:
922 self.manager.retry(
923 client.delete_layer_version,
924 LayerName=r['LayerName'],
925 VersionNumber=r['Version'])
926 except client.exceptions.ResourceNotFound:
927 continue
928
929
930@LambdaLayerVersion.action_registry.register('post-finding')
931class LayerPostFinding(PostFinding):
932
933 resource_type = 'AwsLambdaLayerVersion'
934
935 def format_resource(self, r):
936 envelope, payload = self.format_envelope(r)
937 payload.update(self.filter_empty(
938 select_keys(r, ['Version', 'CreatedDate', 'CompatibleRuntimes'])))
939 return envelope
940
941
942@AWSLambda.filter_registry.register('lambda-edge')
943class LambdaEdgeFilter(Filter):
944 """
945 Filter for lambda@edge functions. Lambda@edge only exists in us-east-1
946
947 :example:
948
949 .. code-block:: yaml
950
951 policies:
952 - name: lambda-edge-filter
953 resource: lambda
954 region: us-east-1
955 filters:
956 - type: lambda-edge
957 state: True
958 """
959 permissions = ('cloudfront:ListDistributions',)
960
961 schema = type_schema('lambda-edge',
962 **{'state': {'type': 'boolean'}})
963
964 def get_lambda_cf_map(self):
965 cfs = self.manager.get_resource_manager('distribution').resources()
966 func_expressions = ('DefaultCacheBehavior.LambdaFunctionAssociations.Items',
967 'CacheBehaviors.Items[].LambdaFunctionAssociations.Items[]')
968 lambda_dist_map = {}
969 for d in cfs:
970 for exp in func_expressions:
971 if jmespath_search(exp, d):
972 for function in jmespath_search(exp, d):
973 # Geting rid of the version number in the arn
974 lambda_edge_arn = ':'.join(function['LambdaFunctionARN'].split(':')[:-1])
975 lambda_dist_map.setdefault(lambda_edge_arn, set()).add(d['Id'])
976 return lambda_dist_map
977
978 def process(self, resources, event=None):
979 results = []
980 if self.manager.config.region != 'us-east-1' and self.data.get('state'):
981 return []
982 annotation_key = 'c7n:DistributionIds'
983 lambda_edge_cf_map = self.get_lambda_cf_map()
984 for r in resources:
985 if (r['FunctionArn'] in lambda_edge_cf_map and self.data.get('state')):
986 r[annotation_key] = list(lambda_edge_cf_map.get(r['FunctionArn']))
987 results.append(r)
988 elif (r['FunctionArn'] not in lambda_edge_cf_map and not self.data.get('state')):
989 results.append(r)
990 return results