1# Copyright The Cloud Custodian Authors.
2# SPDX-License-Identifier: Apache-2.0
3"""
4Application & Network Load Balancers
5"""
6import json
7import logging
8import re
9
10from collections import defaultdict
11from c7n.actions import ActionRegistry, BaseAction, ModifyVpcSecurityGroupsAction
12from c7n.exceptions import PolicyValidationError
13from c7n.filters import (
14 Filter,
15 FilterRegistry,
16 MetricsFilter,
17 ValueFilter,
18 WafV2FilterBase,
19 WafClassicRegionalFilterBase
20)
21import c7n.filters.vpc as net_filters
22from c7n import tags
23from c7n.manager import resources
24
25from c7n.query import QueryResourceManager, DescribeSource, ConfigSource, TypeInfo
26from c7n.utils import (
27 local_session, chunks, type_schema, get_retry, set_annotation)
28
29from c7n.resources.aws import Arn
30from c7n.resources.shield import IsShieldProtected, SetShieldProtection
31
32log = logging.getLogger('custodian.app-elb')
33
34
35class DescribeAppElb(DescribeSource):
36
37 def get_resources(self, ids, cache=True):
38 """Support server side filtering on arns or names
39 """
40 if ids[0].startswith('arn:'):
41 params = {'LoadBalancerArns': ids}
42 else:
43 params = {'Names': ids}
44 return self.query.filter(self.manager, **params)
45
46 def augment(self, albs):
47 _describe_appelb_tags(
48 albs,
49 self.manager.session_factory,
50 self.manager.executor_factory,
51 self.manager.retry)
52
53 return albs
54
55
56class ConfigAppElb(ConfigSource):
57
58 def load_resource(self, item):
59 resource = super(ConfigAppElb, self).load_resource(item)
60 item_attrs = item['supplementaryConfiguration'][
61 'LoadBalancerAttributes']
62 if isinstance(item_attrs, str):
63 item_attrs = json.loads(item_attrs)
64 # Matches annotation of AppELBAttributeFilterBase filter
65 resource['Attributes'] = {
66 attr['key']: parse_attribute_value(attr['value']) for
67 attr in item_attrs}
68 return resource
69
70
71@resources.register('app-elb')
72class AppELB(QueryResourceManager):
73 """Resource manager for v2 ELBs (AKA ALBs and NLBs).
74 """
75
76 class resource_type(TypeInfo):
77 service = 'elbv2'
78 permission_prefix = 'elasticloadbalancing'
79 enum_spec = ('describe_load_balancers', 'LoadBalancers', None)
80 name = 'LoadBalancerName'
81 id = 'LoadBalancerArn'
82 filter_name = "Names"
83 filter_type = "list"
84 dimension = "LoadBalancer"
85 date = 'CreatedTime'
86 cfn_type = config_type = 'AWS::ElasticLoadBalancingV2::LoadBalancer'
87 arn = "LoadBalancerArn"
88 # The suffix varies by type of loadbalancer (app vs net)
89 arn_type = 'loadbalancer/app'
90 permissions_augment = ("elasticloadbalancing:DescribeTags",)
91
92 retry = staticmethod(get_retry(('Throttling',)))
93 source_mapping = {
94 'describe': DescribeAppElb,
95 'config': ConfigAppElb
96 }
97
98 @classmethod
99 def get_permissions(cls):
100 # override as the service is not the iam prefix
101 return ("elasticloadbalancing:DescribeLoadBalancers",
102 "elasticloadbalancing:DescribeLoadBalancerAttributes",
103 "elasticloadbalancing:DescribeTags")
104
105
106def _describe_appelb_tags(albs, session_factory, executor_factory, retry):
107 client = local_session(session_factory).client('elbv2')
108
109 def _process_tags(alb_set):
110 alb_map = {alb['LoadBalancerArn']: alb for alb in alb_set}
111
112 results = retry(client.describe_tags, ResourceArns=list(alb_map.keys()))
113 for tag_desc in results['TagDescriptions']:
114 if ('ResourceArn' in tag_desc and
115 tag_desc['ResourceArn'] in alb_map):
116 alb_map[tag_desc['ResourceArn']]['Tags'] = tag_desc['Tags']
117
118 with executor_factory(max_workers=2) as w:
119 list(w.map(_process_tags, chunks(albs, 20)))
120
121
122AppELB.filter_registry.register('tag-count', tags.TagCountFilter)
123AppELB.filter_registry.register('marked-for-op', tags.TagActionFilter)
124AppELB.filter_registry.register('shield-enabled', IsShieldProtected)
125AppELB.filter_registry.register('network-location', net_filters.NetworkLocation)
126AppELB.action_registry.register('set-shield', SetShieldProtection)
127
128
129@AppELB.filter_registry.register('metrics')
130class AppElbMetrics(MetricsFilter):
131 """Filter app/net load balancer by metric values.
132
133 Note application and network load balancers use different Cloud
134 Watch metrics namespaces and metric names, the custodian app-elb
135 resource returns both types of load balancer, so an additional
136 filter should be used to ensure only targeting a particular
137 type. ie. `- Type: application` or `- Type: network`
138
139 See available application load balancer metrics here
140 https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-cloudwatch-metrics.html
141
142 See available network load balancer metrics here.
143 https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-cloudwatch-metrics.html
144
145
146 For network load balancer metrics, the metrics filter requires specifying
147 the namespace parameter to the filter.
148
149 .. code-block:: yaml
150
151 policies:
152 - name: net-lb-underutilized
153 resource: app-elb
154 filters:
155 - Type: network
156 - type: metrics
157 name: ActiveFlowCount
158 namespace: AWS/NetworkELB
159 statistics: Sum
160 days: 14
161 value: 100
162 op: less-than
163 """
164
165 def get_dimensions(self, resource):
166 return [{
167 'Name': self.model.dimension,
168 'Value': Arn.parse(resource['LoadBalancerArn']).resource}]
169
170
171@AppELB.filter_registry.register('security-group')
172class SecurityGroupFilter(net_filters.SecurityGroupFilter):
173
174 RelatedIdsExpression = "SecurityGroups[]"
175
176
177@AppELB.filter_registry.register('subnet')
178class SubnetFilter(net_filters.SubnetFilter):
179
180 RelatedIdsExpression = "AvailabilityZones[].SubnetId"
181
182
183@AppELB.filter_registry.register('vpc')
184class VpcFilter(net_filters.VpcFilter):
185
186 RelatedIdsExpression = "VpcId"
187
188
189@AppELB.filter_registry.register('waf-enabled')
190class WafEnabled(WafClassicRegionalFilterBase):
191 """Filter Application LoadBalancer by waf-regional web-acl
192
193 :example:
194
195 .. code-block:: yaml
196
197 policies:
198 - name: filter-elb-waf-regional
199 resource: app-elb
200 filters:
201 - type: waf-enabled
202 state: false
203 web-acl: test
204 """
205
206 # application load balancers don't hold a reference to the associated web acl
207 # so we have to look them up via the associations on the web acl directly
208 def get_associated_web_acl(self, resource):
209 return self.get_web_acl_from_associations(
210 'APPLICATION_LOAD_BALANCER',
211 resource['LoadBalancerArn']
212 )
213
214
215@AppELB.filter_registry.register('wafv2-enabled')
216class WafV2Enabled(WafV2FilterBase):
217 """Filter Application LoadBalancer by wafv2 web-acl
218
219 Supports regex expression for web-acl.
220 Firewall Manager pushed WebACL's name varies by account and region.
221 Regex expression can support both local and Firewall Managed WebACL.
222
223 :example:
224
225 .. code-block:: yaml
226
227 policies:
228 - name: filter-wafv2-elb
229 resource: app-elb
230 filters:
231 - type: wafv2-enabled
232 state: false
233 web-acl: testv2
234
235 - name: filter-wafv2-elb-regex
236 resource: app-elb
237 filters:
238 - type: wafv2-enabled
239 state: false
240 web-acl: .*FMManagedWebACLV2-?FMS-.*
241 """
242
243 # application load balancers don't hold a reference to the associated web acl
244 # so we have to look them up via the associations on the web acl directly
245 def get_associated_web_acl(self, resource):
246 return self.get_web_acl_from_associations(
247 'APPLICATION_LOAD_BALANCER',
248 resource['LoadBalancerArn']
249 )
250
251
252@AppELB.action_registry.register('set-waf')
253class SetWaf(BaseAction):
254 """Enable wafv2 protection on Application LoadBalancer.
255
256 :example:
257
258 .. code-block:: yaml
259
260 policies:
261 - name: set-waf-for-elb
262 resource: app-elb
263 filters:
264 - type: waf-enabled
265 state: false
266 web-acl: test
267 actions:
268 - type: set-waf
269 state: true
270 web-acl: test
271
272 - name: disassociate-wafv2-associate-waf-regional-elb
273 resource: app-elb
274 filters:
275 - type: wafv2-enabled
276 state: true
277 actions:
278 - type: set-waf
279 state: true
280 web-acl: test
281
282 """
283 permissions = ('waf-regional:AssociateWebACL', 'waf-regional:ListWebACLs')
284
285 schema = type_schema(
286 'set-waf', required=['web-acl'], **{
287 'web-acl': {'type': 'string'},
288 # 'force': {'type': 'boolean'},
289 'state': {'type': 'boolean'}})
290
291 def validate(self):
292 found = False
293 for f in self.manager.iter_filters():
294 if isinstance(f, WafEnabled) or isinstance(f, WafV2Enabled):
295 found = True
296 break
297 if not found:
298 # try to ensure idempotent usage
299 raise PolicyValidationError(
300 "set-waf should be used in conjunction with waf-enabled or wafv2-enabled \
301 filter on %s" % (self.manager.data,))
302 return self
303
304 def process(self, resources):
305 wafs = self.manager.get_resource_manager('waf-regional').resources(augment=False)
306 name_id_map = {w['Name']: w['WebACLId'] for w in wafs}
307 target_acl = self.data.get('web-acl')
308 target_acl_id = name_id_map.get(target_acl, target_acl)
309 state = self.data.get('state', True)
310
311 if state and target_acl_id not in name_id_map.values():
312 raise ValueError("invalid web acl: %s" % (target_acl_id))
313
314 client = local_session(
315 self.manager.session_factory).client('waf-regional')
316
317 arn_key = self.manager.resource_type.id
318
319 # TODO implement force to reassociate.
320 # TODO investigate limits on waf association.
321 for r in resources:
322 if state:
323 client.associate_web_acl(
324 WebACLId=target_acl_id, ResourceArn=r[arn_key])
325 else:
326 client.disassociate_web_acl(
327 WebACLId=target_acl_id, ResourceArn=r[arn_key])
328
329
330@AppELB.action_registry.register('set-wafv2')
331class SetWafV2(BaseAction):
332 """Enable wafv2 protection on Application LoadBalancer.
333
334 Supports regex expression for web-acl
335
336 :example:
337
338 .. code-block:: yaml
339
340 policies:
341 - name: set-wafv2-for-elb
342 resource: app-elb
343 filters:
344 - type: wafv2-enabled
345 state: false
346 web-acl: testv2
347 actions:
348 - type: set-wafv2
349 state: true
350 web-acl: testv2
351
352 - name: disassociate-waf-regional-associate-wafv2-elb
353 resource: app-elb
354 filters:
355 - type: waf-enabled
356 state: true
357 actions:
358 - type: set-wafv2
359 state: true
360
361 policies:
362 - name: set-wafv2-for-elb-regex
363 resource: app-elb
364 filters:
365 - type: wafv2-enabled
366 state: false
367 web-acl: .*FMManagedWebACLV2-?FMS-.*
368 actions:
369 - type: set-wafv2
370 state: true
371 web-acl: FMManagedWebACLV2-?FMS-TestWebACL
372
373 """
374 permissions = ('wafv2:AssociateWebACL',
375 'wafv2:DisassociateWebACL',
376 'wafv2:ListWebACLs')
377
378 schema = type_schema(
379 'set-wafv2', **{
380 'web-acl': {'type': 'string'},
381 'state': {'type': 'boolean'}})
382
383 retry = staticmethod(get_retry((
384 'ThrottlingException',
385 'RequestLimitExceeded',
386 'Throttled',
387 'ThrottledException',
388 'Throttling',
389 'Client.RequestLimitExceeded')))
390
391 def validate(self):
392 found = False
393 for f in self.manager.iter_filters():
394 if isinstance(f, WafV2Enabled) or isinstance(f, WafEnabled):
395 found = True
396 break
397 if not found:
398 # try to ensure idempotent usage
399 raise PolicyValidationError(
400 "set-wafv2 should be used in conjunction with wafv2-enabled or waf-enabled \
401 filter on %s" % (self.manager.data,))
402 return self
403
404 def process(self, resources):
405 wafs = self.manager.get_resource_manager('wafv2').resources(augment=False)
406 name_id_map = {w['Name']: w['ARN'] for w in wafs}
407 state = self.data.get('state', True)
408
409 target_acl_id = ''
410 if state:
411 target_acl = self.data.get('web-acl', '')
412 target_acl_ids = [v for k, v in name_id_map.items() if
413 re.match(target_acl, k)]
414 if len(target_acl_ids) != 1:
415 raise ValueError(f'{target_acl} matching to none or '
416 f'multiple webacls')
417 target_acl_id = target_acl_ids[0]
418
419 client = local_session(
420 self.manager.session_factory).client('wafv2')
421
422 arn_key = self.manager.resource_type.id
423
424 # TODO implement force to reassociate.
425 # TODO investigate limits on waf association.
426 for r in resources:
427 if state:
428 self.retry(client.associate_web_acl,
429 WebACLArn=target_acl_id,
430 ResourceArn=r[arn_key])
431 else:
432 self.retry(client.disassociate_web_acl,
433 ResourceArn=r[arn_key])
434
435
436@AppELB.action_registry.register('set-s3-logging')
437class SetS3Logging(BaseAction):
438 """Action to enable/disable S3 logging for an application loadbalancer.
439
440 :example:
441
442 .. code-block:: yaml
443
444 policies:
445 - name: elbv2-test
446 resource: app-elb
447 filters:
448 - type: is-not-logging
449 actions:
450 - type: set-s3-logging
451 bucket: elbv2logtest
452 prefix: dahlogs
453 state: enabled
454 """
455 schema = type_schema(
456 'set-s3-logging',
457 state={'enum': ['enabled', 'disabled']},
458 bucket={'type': 'string'},
459 prefix={'type': 'string'},
460 required=('state',))
461
462 permissions = ("elasticloadbalancing:ModifyLoadBalancerAttributes",)
463
464 def validate(self):
465 if self.data.get('state') == 'enabled':
466 if 'bucket' not in self.data or 'prefix' not in self.data:
467 raise PolicyValidationError((
468 "alb logging enablement requires `bucket` "
469 "and `prefix` specification on %s" % (self.manager.data,)))
470 return self
471
472 def process(self, resources):
473 client = local_session(self.manager.session_factory).client('elbv2')
474 for elb in resources:
475 elb_arn = elb['LoadBalancerArn']
476 attributes = [{
477 'Key': 'access_logs.s3.enabled',
478 'Value': (
479 self.data.get('state') == 'enabled' and 'true' or 'value')}]
480
481 if self.data.get('state') == 'enabled':
482 attributes.append({
483 'Key': 'access_logs.s3.bucket',
484 'Value': self.data['bucket']})
485
486 prefix_template = self.data['prefix']
487 info = {t['Key']: t['Value'] for t in elb.get('Tags', ())}
488 info['DNSName'] = elb.get('DNSName', '')
489 info['AccountId'] = elb['LoadBalancerArn'].split(':')[4]
490 info['LoadBalancerName'] = elb['LoadBalancerName']
491
492 attributes.append({
493 'Key': 'access_logs.s3.prefix',
494 'Value': prefix_template.format(**info)})
495
496 self.manager.retry(
497 client.modify_load_balancer_attributes,
498 LoadBalancerArn=elb_arn, Attributes=attributes)
499
500
501@AppELB.action_registry.register('mark-for-op')
502class AppELBMarkForOpAction(tags.TagDelayedAction):
503 """Action to create a delayed action on an ELB to start at a later date
504
505 :example:
506
507 .. code-block:: yaml
508
509 policies:
510 - name: appelb-failed-mark-for-op
511 resource: app-elb
512 filters:
513 - "tag:custodian_elb_cleanup": absent
514 - State: failed
515 actions:
516 - type: mark-for-op
517 tag: custodian_elb_cleanup
518 msg: "AppElb failed: {op}@{action_date}"
519 op: delete
520 days: 1
521 """
522
523 batch_size = 1
524
525
526@AppELB.action_registry.register('tag')
527class AppELBTagAction(tags.Tag):
528 """Action to create tag/tags on an ELB
529
530 :example:
531
532 .. code-block:: yaml
533
534 policies:
535 - name: appelb-create-required-tag
536 resource: app-elb
537 filters:
538 - "tag:RequiredTag": absent
539 actions:
540 - type: tag
541 key: RequiredTag
542 value: RequiredValue
543 """
544
545 batch_size = 1
546 permissions = ("elasticloadbalancing:AddTags",)
547
548 def process_resource_set(self, client, resource_set, ts):
549 client.add_tags(
550 ResourceArns=[alb['LoadBalancerArn'] for alb in resource_set],
551 Tags=ts)
552
553
554@AppELB.action_registry.register('remove-tag')
555class AppELBRemoveTagAction(tags.RemoveTag):
556 """Action to remove tag/tags from an ELB
557
558 :example:
559
560 .. code-block:: yaml
561
562 policies:
563 - name: appelb-delete-expired-tag
564 resource: app-elb
565 filters:
566 - "tag:ExpiredTag": present
567 actions:
568 - type: remove-tag
569 tags: ["ExpiredTag"]
570 """
571
572 batch_size = 1
573 permissions = ("elasticloadbalancing:RemoveTags",)
574
575 def process_resource_set(self, client, resource_set, tag_keys):
576 client.remove_tags(
577 ResourceArns=[alb['LoadBalancerArn'] for alb in resource_set],
578 TagKeys=tag_keys)
579
580
581@AppELB.action_registry.register('delete')
582class AppELBDeleteAction(BaseAction):
583 """Action to delete an ELB
584
585 To avoid unwanted deletions of ELB, it is recommended to apply a filter
586 to the rule
587
588 :example:
589
590 .. code-block:: yaml
591
592 policies:
593 - name: appelb-delete-failed-elb
594 resource: app-elb
595 filters:
596 - State: failed
597 actions:
598 - delete
599 """
600
601 schema = type_schema('delete', force={'type': 'boolean'})
602 permissions = (
603 "elasticloadbalancing:DeleteLoadBalancer",
604 "elasticloadbalancing:ModifyLoadBalancerAttributes",)
605
606 def process(self, load_balancers):
607 client = local_session(self.manager.session_factory).client('elbv2')
608 for lb in load_balancers:
609 self.process_alb(client, lb)
610
611 def process_alb(self, client, alb):
612 try:
613 if self.data.get('force'):
614 client.modify_load_balancer_attributes(
615 LoadBalancerArn=alb['LoadBalancerArn'],
616 Attributes=[{
617 'Key': 'deletion_protection.enabled',
618 'Value': 'false',
619 }])
620 self.manager.retry(
621 client.delete_load_balancer, LoadBalancerArn=alb['LoadBalancerArn'])
622 except client.exceptions.LoadBalancerNotFoundException:
623 pass
624 except (
625 client.exceptions.OperationNotPermittedException,
626 client.exceptions.ResourceInUseException
627 ) as e:
628 self.log.warning(
629 "Exception trying to delete load balancer: %s error: %s",
630 alb['LoadBalancerArn'], e)
631
632
633@AppELB.action_registry.register('modify-attributes')
634class AppELBModifyAttributes(BaseAction):
635 """Modify load balancer attributes.
636
637 :example:
638
639 .. code-block:: yaml
640
641 policies:
642 - name: turn-on-elb-deletion-protection
643 resource: app-elb
644 filters:
645 - type: attributes
646 key: "deletion_protection.enabled"
647 value: false
648 actions:
649 - type: modify-attributes
650 attributes:
651 "deletion_protection.enabled": "true"
652 "idle_timeout.timeout_seconds": 120
653 """
654 schema = {
655 'type': 'object',
656 'additionalProperties': False,
657 'properties': {
658 'type': {
659 'enum': ['modify-attributes']},
660 'attributes': {
661 'type': 'object',
662 'additionalProperties': False,
663 'properties': {
664 'access_logs.s3.enabled': {
665 'enum': ['true', 'false', True, False]},
666 'access_logs.s3.bucket': {'type': 'string'},
667 'access_logs.s3.prefix': {'type': 'string'},
668 'deletion_protection.enabled': {
669 'enum': ['true', 'false', True, False]},
670 'idle_timeout.timeout_seconds': {'type': 'number'},
671 'routing.http.desync_mitigation_mode': {
672 'enum': ['monitor', 'defensive', 'strictest']},
673 'routing.http.drop_invalid_header_fields.enabled': {
674 'enum': ['true', 'false', True, False]},
675 'routing.http2.enabled': {
676 'enum': ['true', 'false', True, False]},
677 'load_balancing.cross_zone.enabled': {
678 'enum': ['true', 'false', True, False]},
679 },
680 },
681 },
682 }
683 permissions = ("elasticloadbalancing:ModifyLoadBalancerAttributes",)
684
685 def process(self, resources):
686 client = local_session(self.manager.session_factory).client('elbv2')
687 for appelb in resources:
688 self.manager.retry(
689 client.modify_load_balancer_attributes,
690 LoadBalancerArn=appelb['LoadBalancerArn'],
691 Attributes=[
692 {'Key': key, 'Value': serialize_attribute_value(value)}
693 for (key, value) in self.data['attributes'].items()
694 ],
695 ignore_err_codes=('LoadBalancerNotFoundException',),
696 )
697 return resources
698
699
700class AppELBListenerFilterBase:
701 """ Mixin base class for filters that query LB listeners.
702 """
703 permissions = ("elasticloadbalancing:DescribeListeners",)
704
705 def initialize(self, albs):
706 client = local_session(self.manager.session_factory).client('elbv2')
707 self.listener_map = defaultdict(list)
708 for alb in albs:
709 results = self.manager.retry(client.describe_listeners,
710 LoadBalancerArn=alb['LoadBalancerArn'],
711 ignore_err_codes=('LoadBalancerNotFoundException',))
712 self.listener_map[alb['LoadBalancerArn']] = results['Listeners']
713
714
715def parse_attribute_value(v):
716 if v.isdigit():
717 v = int(v)
718 elif v == 'true':
719 v = True
720 elif v == 'false':
721 v = False
722 return v
723
724
725def serialize_attribute_value(v):
726 if v is True:
727 return 'true'
728 elif v is False:
729 return 'false'
730 elif isinstance(v, int):
731 return str(v)
732 return v
733
734
735class AppELBAttributeFilterBase:
736 """ Mixin base class for filters that query LB attributes.
737 """
738
739 def initialize(self, albs):
740 client = local_session(self.manager.session_factory).client('elbv2')
741
742 def _process_attributes(alb):
743 if 'Attributes' not in alb:
744 alb['Attributes'] = {}
745 results = client.describe_load_balancer_attributes(
746 LoadBalancerArn=alb['LoadBalancerArn'])
747 # flatten out the list of dicts and cast
748 for pair in results['Attributes']:
749 k = pair['Key']
750 v = parse_attribute_value(pair['Value'])
751 alb['Attributes'][k] = v
752
753 with self.manager.executor_factory(max_workers=2) as w:
754 list(w.map(_process_attributes, albs))
755
756
757@AppELB.filter_registry.register('is-logging')
758class IsLoggingFilter(Filter, AppELBAttributeFilterBase):
759 """ Matches AppELBs that are logging to S3.
760 bucket and prefix are optional
761
762 :example:
763
764 .. code-block:: yaml
765
766 policies:
767 - name: alb-is-logging-test
768 resource: app-elb
769 filters:
770 - type: is-logging
771
772 - name: alb-is-logging-bucket-and-prefix-test
773 resource: app-elb
774 filters:
775 - type: is-logging
776 bucket: prodlogs
777 prefix: alblogs
778
779 """
780 permissions = ("elasticloadbalancing:DescribeLoadBalancerAttributes",)
781 schema = type_schema('is-logging',
782 bucket={'type': 'string'},
783 prefix={'type': 'string'}
784 )
785
786 def process(self, resources, event=None):
787 self.initialize(resources)
788 bucket_name = self.data.get('bucket', None)
789 bucket_prefix = self.data.get('prefix', None)
790
791 return [alb for alb in resources
792 if alb['Attributes']['access_logs.s3.enabled'] and
793 (not bucket_name or bucket_name == alb['Attributes'].get(
794 'access_logs.s3.bucket', None)) and
795 (not bucket_prefix or bucket_prefix == alb['Attributes'].get(
796 'access_logs.s3.prefix', None))
797 ]
798
799
800@AppELB.filter_registry.register('is-not-logging')
801class IsNotLoggingFilter(Filter, AppELBAttributeFilterBase):
802 """ Matches AppELBs that are NOT logging to S3.
803 or do not match the optional bucket and/or prefix.
804
805 :example:
806
807 .. code-block:: yaml
808
809 policies:
810 - name: alb-is-not-logging-test
811 resource: app-elb
812 filters:
813 - type: is-not-logging
814
815 - name: alb-is-not-logging-bucket-and-prefix-test
816 resource: app-elb
817 filters:
818 - type: is-not-logging
819 bucket: prodlogs
820 prefix: alblogs
821
822 """
823 permissions = ("elasticloadbalancing:DescribeLoadBalancerAttributes",)
824 schema = type_schema('is-not-logging',
825 bucket={'type': 'string'},
826 prefix={'type': 'string'}
827 )
828
829 def process(self, resources, event=None):
830 self.initialize(resources)
831 bucket_name = self.data.get('bucket', None)
832 bucket_prefix = self.data.get('prefix', None)
833
834 return [alb for alb in resources
835 if not alb['Attributes']['access_logs.s3.enabled'] or
836 (bucket_name and bucket_name != alb['Attributes'].get(
837 'access_logs.s3.bucket', None)) or
838 (bucket_prefix and bucket_prefix != alb['Attributes'].get(
839 'access_logs.s3.prefix', None))]
840
841
842@AppELB.filter_registry.register('attributes')
843class CheckAttributes(ValueFilter, AppELBAttributeFilterBase):
844 """ Value filter that allows filtering on ELBv2 attributes
845
846 :example:
847
848 .. code-block:: yaml
849
850 policies:
851 - name: alb-http2-enabled
852 resource: app-elb
853 filters:
854 - type: attributes
855 key: routing.http2.enabled
856 value: true
857 op: eq
858 """
859 annotate: False # no annotation from value Filter
860 permissions = ("elasticloadbalancing:DescribeLoadBalancerAttributes",)
861 schema = type_schema('attributes', rinherit=ValueFilter.schema)
862 schema_alias = False
863
864 def process(self, resources, event=None):
865 self.augment(resources)
866 return super().process(resources, event)
867
868 def augment(self, resources):
869 self.initialize(resources)
870
871 def __call__(self, r):
872 return super().__call__(r['Attributes'])
873
874
875class AppELBTargetGroupFilterBase:
876 """ Mixin base class for filters that query LB target groups.
877 """
878
879 def initialize(self, albs):
880 self.target_group_map = defaultdict(list)
881 target_groups = self.manager.get_resource_manager(
882 'app-elb-target-group').resources()
883 for target_group in target_groups:
884 for load_balancer_arn in target_group['LoadBalancerArns']:
885 self.target_group_map[load_balancer_arn].append(target_group)
886
887
888@AppELB.filter_registry.register('listener')
889class AppELBListenerFilter(ValueFilter, AppELBListenerFilterBase):
890 """Filter ALB based on matching listener attributes
891
892 Adding the `matched` flag will filter on previously matched listeners
893
894 :example:
895
896 .. code-block:: yaml
897
898 policies:
899 - name: app-elb-invalid-ciphers
900 resource: app-elb
901 filters:
902 - type: listener
903 key: Protocol
904 value: HTTPS
905 - type: listener
906 key: SslPolicy
907 value: ['ELBSecurityPolicy-TLS-1-1-2017-01','ELBSecurityPolicy-TLS-1-2-2017-01']
908 op: ni
909 matched: true
910 actions:
911 - type: modify-listener
912 sslpolicy: "ELBSecurityPolicy-TLS-1-2-2017-01"
913 """
914
915 schema = type_schema(
916 'listener', rinherit=ValueFilter.schema, matched={'type': 'boolean'})
917 schema_alias = False
918 permissions = ("elasticloadbalancing:DescribeLoadBalancerAttributes",)
919
920 def validate(self):
921 if not self.data.get('matched'):
922 return
923 listeners = list(self.manager.iter_filters())
924 found = False
925 for f in listeners[:listeners.index(self)]:
926 if not f.data.get('matched', False):
927 found = True
928 break
929 if not found:
930 raise PolicyValidationError(
931 "matched listener filter, requires preceding listener filter on %s " % (
932 self.manager.data,))
933 return self
934
935 def process(self, albs, event=None):
936 self.initialize(albs)
937 return super(AppELBListenerFilter, self).process(albs, event)
938
939 def __call__(self, alb):
940 listeners = self.listener_map[alb['LoadBalancerArn']]
941 if self.data.get('matched', False):
942 listeners = alb.pop('c7n:MatchedListeners', [])
943
944 found_listeners = False
945 for listener in listeners:
946 if self.match(listener):
947 set_annotation(alb, 'c7n:MatchedListeners', listener)
948 found_listeners = True
949 return found_listeners
950
951
952@AppELB.action_registry.register('modify-listener')
953class AppELBModifyListenerPolicy(BaseAction):
954 """Action to modify the policy for an App ELB
955
956 :example:
957
958 .. code-block:: yaml
959
960 policies:
961 - name: appelb-modify-listener
962 resource: app-elb
963 filters:
964 - type: listener
965 key: Protocol
966 value: HTTP
967 actions:
968 - type: modify-listener
969 protocol: HTTPS
970 sslpolicy: "ELBSecurityPolicy-TLS-1-2-2017-01"
971 certificate: "arn:aws:acm:region:123456789012:certificate/12345678-\
972 1234-1234-1234-123456789012"
973 """
974
975 schema = type_schema(
976 'modify-listener',
977 port={'type': 'integer'},
978 protocol={'enum': ['HTTP', 'HTTPS', 'TCP', 'TLS', 'UDP', 'TCP_UDP', 'GENEVE']},
979 sslpolicy={'type': 'string'},
980 certificate={'type': 'string'}
981 )
982
983 permissions = ("elasticloadbalancing:ModifyListener",)
984
985 def validate(self):
986 for f in self.manager.iter_filters():
987 if f.type == 'listener':
988 return self
989 raise PolicyValidationError(
990 "modify-listener action requires the listener filter %s" % (
991 self.manager.data,))
992
993 def process(self, load_balancers):
994 args = {}
995 if 'port' in self.data:
996 args['Port'] = self.data.get('port')
997 if 'protocol' in self.data:
998 args['Protocol'] = self.data.get('protocol')
999 if 'sslpolicy' in self.data:
1000 args['SslPolicy'] = self.data.get('sslpolicy')
1001 if 'certificate' in self.data:
1002 args['Certificates'] = [{'CertificateArn': self.data.get('certificate')}]
1003 client = local_session(self.manager.session_factory).client('elbv2')
1004
1005 for alb in load_balancers:
1006 for matched_listener in alb.get('c7n:MatchedListeners', ()):
1007 client.modify_listener(
1008 ListenerArn=matched_listener['ListenerArn'],
1009 **args)
1010
1011
1012@AppELB.action_registry.register('modify-security-groups')
1013class AppELBModifyVpcSecurityGroups(ModifyVpcSecurityGroupsAction):
1014
1015 permissions = ("elasticloadbalancing:SetSecurityGroups",)
1016
1017 def process(self, albs):
1018 client = local_session(self.manager.session_factory).client('elbv2')
1019 groups = super(AppELBModifyVpcSecurityGroups, self).get_groups(albs)
1020
1021 for idx, i in enumerate(albs):
1022 try:
1023 client.set_security_groups(
1024 LoadBalancerArn=i['LoadBalancerArn'],
1025 SecurityGroups=groups[idx])
1026 except client.exceptions.LoadBalancerNotFoundException:
1027 continue
1028
1029
1030@AppELB.filter_registry.register('healthcheck-protocol-mismatch')
1031class AppELBHealthCheckProtocolMismatchFilter(Filter,
1032 AppELBTargetGroupFilterBase):
1033 """Filter AppELBs with mismatched health check protocols
1034
1035 A mismatched health check protocol is where the protocol on the target group
1036 does not match the load balancer health check protocol
1037
1038 :example:
1039
1040 .. code-block:: yaml
1041
1042 policies:
1043 - name: appelb-healthcheck-mismatch
1044 resource: app-elb
1045 filters:
1046 - healthcheck-protocol-mismatch
1047 """
1048
1049 schema = type_schema('healthcheck-protocol-mismatch')
1050 permissions = ("elasticloadbalancing:DescribeTargetGroups",)
1051
1052 def process(self, albs, event=None):
1053 def _healthcheck_protocol_mismatch(alb):
1054 for target_group in self.target_group_map[alb['LoadBalancerArn']]:
1055 if (target_group['Protocol'] !=
1056 target_group['HealthCheckProtocol']):
1057 return True
1058
1059 return False
1060
1061 self.initialize(albs)
1062 return [alb for alb in albs if _healthcheck_protocol_mismatch(alb)]
1063
1064
1065@AppELB.filter_registry.register('target-group')
1066class AppELBTargetGroupFilter(ValueFilter, AppELBTargetGroupFilterBase):
1067 """Filter ALB based on matching target group value"""
1068
1069 schema = type_schema('target-group', rinherit=ValueFilter.schema)
1070 schema_alias = False
1071 permissions = ("elasticloadbalancing:DescribeTargetGroups",)
1072
1073 def process(self, albs, event=None):
1074 self.initialize(albs)
1075 return super(AppELBTargetGroupFilter, self).process(albs, event)
1076
1077 def __call__(self, alb):
1078 target_groups = self.target_group_map[alb['LoadBalancerArn']]
1079 return self.match(target_groups)
1080
1081
1082@AppELB.filter_registry.register('default-vpc')
1083class AppELBDefaultVpcFilter(net_filters.DefaultVpcBase):
1084 """Filter all ELB that exist within the default vpc
1085
1086 :example:
1087
1088 .. code-block:: yaml
1089
1090 policies:
1091 - name: appelb-in-default-vpc
1092 resource: app-elb
1093 filters:
1094 - default-vpc
1095 """
1096
1097 schema = type_schema('default-vpc')
1098
1099 def __call__(self, alb):
1100 return alb.get('VpcId') and self.match(alb.get('VpcId')) or False
1101
1102
1103@resources.register('app-elb-target-group')
1104class AppELBTargetGroup(QueryResourceManager):
1105 """Resource manager for v2 ELB target groups.
1106 """
1107
1108 class resource_type(TypeInfo):
1109 service = 'elbv2'
1110 arn_type = 'target-group'
1111 enum_spec = ('describe_target_groups', 'TargetGroups', None)
1112 name = 'TargetGroupName'
1113 id = 'TargetGroupArn'
1114 permission_prefix = 'elasticloadbalancing'
1115 cfn_type = 'AWS::ElasticLoadBalancingV2::TargetGroup'
1116
1117 filter_registry = FilterRegistry('app-elb-target-group.filters')
1118 action_registry = ActionRegistry('app-elb-target-group.actions')
1119 retry = staticmethod(get_retry(('Throttling',)))
1120
1121 filter_registry.register('tag-count', tags.TagCountFilter)
1122 filter_registry.register('marked-for-op', tags.TagActionFilter)
1123
1124 @classmethod
1125 def get_permissions(cls):
1126 # override as the service is not the iam prefix
1127 return ("elasticloadbalancing:DescribeTargetGroups",
1128 "elasticloadbalancing:DescribeTags")
1129
1130 def augment(self, target_groups):
1131 client = local_session(self.session_factory).client('elbv2')
1132
1133 def _describe_target_group_health(target_group):
1134 result = self.retry(client.describe_target_health,
1135 TargetGroupArn=target_group['TargetGroupArn'])
1136 target_group['TargetHealthDescriptions'] = result[
1137 'TargetHealthDescriptions']
1138
1139 with self.executor_factory(max_workers=2) as w:
1140 list(w.map(_describe_target_group_health, target_groups))
1141
1142 _describe_target_group_tags(
1143 target_groups, self.session_factory,
1144 self.executor_factory, self.retry)
1145 return target_groups
1146
1147
1148def _describe_target_group_tags(target_groups, session_factory,
1149 executor_factory, retry):
1150 client = local_session(session_factory).client('elbv2')
1151
1152 def _process_tags(target_group_set):
1153 target_group_map = {
1154 target_group['TargetGroupArn']:
1155 target_group for target_group in target_group_set
1156 }
1157
1158 results = retry(
1159 client.describe_tags,
1160 ResourceArns=list(target_group_map.keys()))
1161 for tag_desc in results['TagDescriptions']:
1162 if ('ResourceArn' in tag_desc and
1163 tag_desc['ResourceArn'] in target_group_map):
1164 target_group_map[
1165 tag_desc['ResourceArn']
1166 ]['Tags'] = tag_desc['Tags']
1167
1168 with executor_factory(max_workers=2) as w:
1169 list(w.map(_process_tags, chunks(target_groups, 20)))
1170
1171
1172@AppELBTargetGroup.action_registry.register('mark-for-op')
1173class AppELBTargetGroupMarkForOpAction(tags.TagDelayedAction):
1174 """Action to specify a delayed action on an ELB target group"""
1175
1176
1177@AppELBTargetGroup.action_registry.register('tag')
1178class AppELBTargetGroupTagAction(tags.Tag):
1179 """Action to create tag/tags on an ELB target group
1180
1181 :example:
1182
1183 .. code-block:: yaml
1184
1185 policies:
1186 - name: appelb-targetgroup-add-required-tag
1187 resource: app-elb-target-group
1188 filters:
1189 - "tag:RequiredTag": absent
1190 actions:
1191 - type: tag
1192 key: RequiredTag
1193 value: RequiredValue
1194 """
1195
1196 batch_size = 1
1197 permissions = ("elasticloadbalancing:AddTags",)
1198
1199 def process_resource_set(self, client, resource_set, ts):
1200 client.add_tags(
1201 ResourceArns=[tgroup['TargetGroupArn'] for tgroup in resource_set],
1202 Tags=ts)
1203
1204
1205@AppELBTargetGroup.action_registry.register('remove-tag')
1206class AppELBTargetGroupRemoveTagAction(tags.RemoveTag):
1207 """Action to remove tag/tags from ELB target group
1208
1209 :example:
1210
1211 .. code-block:: yaml
1212
1213 policies:
1214 - name: appelb-targetgroup-remove-expired-tag
1215 resource: app-elb-target-group
1216 filters:
1217 - "tag:ExpiredTag": present
1218 actions:
1219 - type: remove-tag
1220 tags: ["ExpiredTag"]
1221 """
1222
1223 batch_size = 1
1224 permissions = ("elasticloadbalancing:RemoveTags",)
1225
1226 def process_resource_set(self, client, resource_set, tag_keys):
1227 client.remove_tags(
1228 ResourceArns=[tgroup['TargetGroupArn'] for tgroup in resource_set],
1229 TagKeys=tag_keys)
1230
1231
1232@AppELBTargetGroup.filter_registry.register('default-vpc')
1233class AppELBTargetGroupDefaultVpcFilter(net_filters.DefaultVpcBase):
1234 """Filter all application elb target groups within the default vpc
1235
1236 :example:
1237
1238 .. code-block:: yaml
1239
1240 policies:
1241 - name: appelb-targetgroups-default-vpc
1242 resource: app-elb-target-group
1243 filters:
1244 - default-vpc
1245 """
1246
1247 schema = type_schema('default-vpc')
1248
1249 def __call__(self, target_group):
1250 return (target_group.get('VpcId') and
1251 self.match(target_group.get('VpcId')) or False)
1252
1253
1254@AppELBTargetGroup.action_registry.register('delete')
1255class AppELBTargetGroupDeleteAction(BaseAction):
1256 """Action to delete ELB target group
1257
1258 It is recommended to apply a filter to the delete policy to avoid unwanted
1259 deletion of any app elb target groups.
1260
1261 :example:
1262
1263 .. code-block:: yaml
1264
1265 policies:
1266 - name: appelb-targetgroups-delete-unused
1267 resource: app-elb-target-group
1268 filters:
1269 - "tag:SomeTag": absent
1270 actions:
1271 - delete
1272 """
1273
1274 schema = type_schema('delete')
1275 permissions = ('elasticloadbalancing:DeleteTargetGroup',)
1276
1277 def process(self, resources):
1278 client = local_session(self.manager.session_factory).client('elbv2')
1279 for tg in resources:
1280 self.process_target_group(client, tg)
1281
1282 def process_target_group(self, client, target_group):
1283 self.manager.retry(
1284 client.delete_target_group,
1285 TargetGroupArn=target_group['TargetGroupArn'])
1286
1287
1288class TargetGroupAttributeFilterBase:
1289 """ Mixin base class for filters that query Target Group attributes.
1290 """
1291
1292 def initialize(self, tgs):
1293 client = local_session(self.manager.session_factory).client('elbv2')
1294
1295 def _process_attributes(tg):
1296 if 'c7n:TargetGroupAttributes' not in tg:
1297 tg['c7n:TargetGroupAttributes'] = {}
1298 results = self.manager.retry(client.describe_target_group_attributes,
1299 TargetGroupArn=tg['TargetGroupArn'],
1300 ignore_err_codes=('TargetGroupNotFoundException',))
1301 # flatten out the list of dicts and cast
1302 for pair in results['Attributes']:
1303 k = pair['Key']
1304 v = parse_attribute_value(pair['Value'])
1305 tg['c7n:TargetGroupAttributes'][k] = v
1306
1307 with self.manager.executor_factory(max_workers=2) as w:
1308 list(w.map(_process_attributes, tgs))
1309
1310
1311@AppELBTargetGroup.filter_registry.register('attributes')
1312class TargetGroupCheckAttributes(ValueFilter, TargetGroupAttributeFilterBase):
1313 """ Value filter that allows filtering on Target group attributes
1314
1315 :example:
1316
1317 .. code-block:: yaml
1318
1319 policies:
1320 - name: target-group-check-attributes
1321 resource: app-elb-target-group
1322 filters:
1323 - type: attributes
1324 key: preserve_client_ip.enabled
1325 value: True
1326 op: eq
1327 """
1328 annotate: False # no annotation from value Filter
1329 permissions = ("elasticloadbalancing:DescribeTargetGroupAttributes",)
1330 schema = type_schema('attributes', rinherit=ValueFilter.schema)
1331 schema_alias = False
1332
1333 def process(self, resources, event=None):
1334 self.augment(resources)
1335 return super().process(resources, event)
1336
1337 def augment(self, resources):
1338 self.initialize(resources)
1339
1340 def __call__(self, r):
1341 return super().__call__(r['c7n:TargetGroupAttributes'])
1342
1343
1344@AppELBTargetGroup.action_registry.register('modify-attributes')
1345class AppELBTargetGroupModifyAttributes(BaseAction):
1346 """Modify target group attributes.
1347
1348 :example:
1349
1350 .. code-block:: yaml
1351
1352 policies:
1353 - name: modify-preserve-client-ip-enable
1354 resource: app-elb-target-group
1355 filters:
1356 - type: attributes
1357 key: "preserve_client_ip.enabled"
1358 value: False
1359 actions:
1360 - type: modify-attributes
1361 attributes:
1362 "preserve_client_ip.enabled": "true"
1363 """
1364 schema = {
1365 'type': 'object',
1366 'additionalProperties': False,
1367 'properties': {
1368 'type': {
1369 'enum': ['modify-attributes']},
1370 'attributes': {
1371 'type': 'object',
1372 'additionalProperties': False,
1373 'properties': {
1374 'proxy_protocol_v2.enabled': {
1375 'enum': ['true', 'false', True, False]},
1376 'preserve_client_ip.enabled': {
1377 'enum': ['true', 'false', True, False]},
1378 'stickiness.enabled': {
1379 'enum': ['true', 'false', True, False]},
1380 'lambda.multi_value_headers.enabled': {
1381 'enum': ['true', 'false', True, False]},
1382 'deregistration_delay.connection_termination.enabled': {
1383 'enum': ['true', 'false', True, False]},
1384 'target_group_health.unhealthy_state_routing.'
1385 'minimum_healthy_targets.count': {'type': 'number'},
1386 'target_group_health.unhealthy_state_routing.'
1387 'minimum_healthy_targets.percentage': {'type': 'string'},
1388 'deregistration_delay.timeout_seconds': {'type': 'number'},
1389 'target_group_health.dns_failover.minimum_healthy_targets.count': {
1390 'type': 'string'},
1391 'stickiness.type': {
1392 'enum': ['lb_cookie', 'app_cookie', 'source_ip',
1393 'source_ip_dest_ip', 'source_ip_dest_ip_proto']},
1394 'load_balancing.cross_zone.enabled': {
1395 'enum': ['true', 'false', True, False, 'use_load_balancer_configuration']},
1396 'target_group_health.dns_failover.minimum_healthy_targets.percentage': {
1397 'type': 'string'},
1398 'stickiness.app_cookie.cookie_name': {'type': 'string'},
1399 'stickiness.lb_cookie.duration_seconds': {'type': 'number'},
1400 'slow_start.duration_seconds': {'type': 'number'},
1401 'stickiness.app_cookie.duration_seconds': {'type': 'number'},
1402 'load_balancing.algorithm.type': {
1403 'enum': ['round_robin', 'least_outstanding_requests']},
1404 'target_failover.on_deregistration': {
1405 'enum': ['rebalance', 'no_rebalance']},
1406 'target_failover.on_unhealthy': {
1407 'enum': ['rebalance', 'no_rebalance']},
1408 },
1409 },
1410 },
1411 }
1412 permissions = ("elasticloadbalancing:ModifyTargetGroupAttributes",)
1413
1414 def process(self, resources):
1415 client = local_session(self.manager.session_factory).client('elbv2')
1416 self.log.info(resources)
1417 for targetgroup in resources:
1418 self.manager.retry(
1419 client.modify_target_group_attributes,
1420 TargetGroupArn=targetgroup['TargetGroupArn'],
1421 Attributes=[
1422 {'Key': key, 'Value': serialize_attribute_value(value)}
1423 for (key, value) in self.data['attributes'].items()
1424 ],
1425 ignore_err_codes=('TargetGroupNotFoundException',),
1426 )
1427 return resources