1# Copyright The Cloud Custodian Authors.
2# SPDX-License-Identifier: Apache-2.0
3import itertools
4import zlib
5import re
6from c7n.actions import BaseAction, ModifyVpcSecurityGroupsAction
7from c7n.deprecated import DeprecatedField
8from c7n.exceptions import PolicyValidationError, ClientError
9from c7n.filters import Filter, ValueFilter, MetricsFilter, ListItemFilter
10import c7n.filters.vpc as net_filters
11from c7n.filters.iamaccess import CrossAccountAccessFilter
12from c7n.filters.related import RelatedResourceFilter, RelatedResourceByIdFilter
13from c7n.filters.revisions import Diff
14from c7n import query, resolver
15from c7n.manager import resources
16from c7n.resources.securityhub import OtherResourcePostFinding, PostFinding
17from c7n.utils import (
18 chunks,
19 get_eni_resource_type,
20 get_retry,
21 jmespath_compile,
22 jmespath_search,
23 local_session,
24 merge_dict,
25 parse_cidr,
26 type_schema,
27)
28from c7n.resources.aws import shape_validate
29from c7n.resources.shield import IsEIPShieldProtected, SetEIPShieldProtection
30from c7n.filters.policystatement import HasStatementFilter
31
32
33@resources.register('vpc')
34class Vpc(query.QueryResourceManager):
35
36 class resource_type(query.TypeInfo):
37 service = 'ec2'
38 arn_type = 'vpc'
39 enum_spec = ('describe_vpcs', 'Vpcs', None)
40 name = id = 'VpcId'
41 filter_name = 'VpcIds'
42 filter_type = 'list'
43 cfn_type = config_type = 'AWS::EC2::VPC'
44 id_prefix = "vpc-"
45
46
47@Vpc.filter_registry.register('metrics')
48class VpcMetrics(MetricsFilter):
49
50 def get_dimensions(self, resource):
51 return [{"Name": "Per-VPC Metrics",
52 "Value": resource["VpcId"]}]
53
54
55@Vpc.action_registry.register('modify')
56class ModifyVpc(BaseAction):
57 """Modify vpc settings
58 """
59
60 schema = type_schema(
61 'modify',
62 **{'dnshostnames': {'type': 'boolean'},
63 'dnssupport': {'type': 'boolean'},
64 'addressusage': {'type': 'boolean'}}
65 )
66
67 key_params = (
68 ('dnshostnames', 'EnableDnsHostnames'),
69 ('dnssupport', 'EnableDnsSupport'),
70 ('addressusage', 'EnableNetworkAddressUsageMetrics')
71 )
72
73 permissions = ('ec2:ModifyVpcAttribute',)
74
75 def process(self, resources):
76 client = local_session(self.manager.session_factory).client('ec2')
77
78 for policy_key, param_name in self.key_params:
79 if policy_key not in self.data:
80 continue
81 params = {param_name: {'Value': self.data[policy_key]}}
82 # can only modify one attribute per request
83 for r in resources:
84 params['VpcId'] = r['VpcId']
85 client.modify_vpc_attribute(**params)
86
87
88@Vpc.action_registry.register('delete-empty')
89class DeleteVpc(BaseAction):
90 """Delete an empty VPC
91
92 For example, if you want to delete an empty VPC
93
94 :example:
95
96 .. code-block:: yaml
97
98 - name: aws-ec2-vpc-delete
99 resource: vpc
100 actions:
101 - type: delete-empty
102
103 """
104 schema = type_schema('delete-empty',)
105 permissions = ('ec2:DeleteVpc',)
106
107 def process(self, resources):
108 client = local_session(self.manager.session_factory).client('ec2')
109
110 for vpc in resources:
111 self.manager.retry(
112 client.delete_vpc,
113 VpcId=vpc['VpcId'],
114 ignore_err_codes=(
115 'NoSuchEntityException',
116 'DeleteConflictException',
117 ),
118 )
119
120
121class DescribeFlow(query.DescribeSource):
122
123 def get_resources(self, ids, cache=True):
124 params = {'Filters': [{'Name': 'flow-log-id', 'Values': ids}]}
125 return self.query.filter(self.resource_manager, **params)
126
127
128@resources.register('flow-log')
129class FlowLog(query.QueryResourceManager):
130
131 class resource_type(query.TypeInfo):
132
133 service = 'ec2'
134 arn_type = 'vpc-flow-log'
135 enum_spec = ('describe_flow_logs', 'FlowLogs', None)
136 name = id = 'FlowLogId'
137 cfn_type = config_type = 'AWS::EC2::FlowLog'
138 id_prefix = 'fl-'
139
140 source_mapping = {
141 'describe': DescribeFlow,
142 'config': query.ConfigSource
143 }
144
145
146@Vpc.filter_registry.register('flow-logs')
147class FlowLogv2Filter(ListItemFilter):
148 """Are flow logs enabled on the resource.
149
150 This filter reuses `list-item` filter for arbitrary filtering
151 on the flow log attibutes, it also maintains compatiblity
152 with the legacy flow-log filter.
153
154 ie to find all vpcs with flows logs disabled we can do this
155
156 :example:
157
158 .. code-block:: yaml
159
160 policies:
161 - name: flow-logs-enabled
162 resource: vpc
163 filters:
164 - flow-logs
165
166 or to find all vpcs with flow logs but that don't match a
167 particular configuration.
168
169 :example:
170
171 .. code-block:: yaml
172
173 policies:
174 - name: flow-mis-configured
175 resource: vpc
176 filters:
177 - not:
178 - type: flow-logs
179 attrs:
180 - TrafficType: ALL
181 - FlowLogStatus: ACTIVE
182 - LogGroupName: vpc-logs
183 """
184
185 legacy_schema = {
186 'enabled': {'type': 'boolean', 'default': False},
187 'op': {'enum': ['equal', 'not-equal'], 'default': 'equal'},
188 'set-op': {'enum': ['or', 'and'], 'default': 'or'},
189 'status': {'enum': ['active']},
190 'deliver-status': {'enum': ['success', 'failure']},
191 'destination': {'type': 'string'},
192 'destination-type': {'enum': ['s3', 'cloud-watch-logs']},
193 'traffic-type': {'enum': ['accept', 'reject', 'all']},
194 'log-format': {'type': 'string'},
195 'log-group': {'type': 'string'}
196 }
197
198 schema = type_schema(
199 'flow-logs',
200 attrs={'$ref': '#/definitions/filters_common/list_item_attrs'},
201 count={'type': 'number'},
202 count_op={'$ref': '#/definitions/filters_common/comparison_operators'},
203 **legacy_schema
204 )
205 schema_alias = True
206 annotate_items = True
207 permissions = ('ec2:DescribeFlowLogs',)
208
209 compat_conversion = {
210 'status': {
211 'key': 'FlowLogStatus',
212 'values': {'active': 'ACTIVE'},
213 },
214 'deliver-status': {
215 'key': 'DeliverLogsStatus',
216 'values': {'success': 'SUCCESS',
217 'failure': 'FAILED'}
218 },
219 'destination': {
220 'key': 'LogDestination',
221 },
222 'destination-type': {
223 'key': 'LogDestinationType',
224 # values ?
225 },
226 'traffic-type': {
227 'key': 'TrafficType',
228 'values': {'all': 'ALL',
229 'reject': 'REJECT',
230 'accept': 'ACCEPT'},
231 },
232 'log-format': {
233 'key': 'LogFormat',
234 },
235 'log-group': {
236 'key': 'LogGroupName'
237 }
238 }
239
240 flow_log_map = None
241
242 def get_deprecations(self):
243 filter_name = self.data["type"]
244 return [
245 DeprecatedField(f"{filter_name}.{k}", "use list-item style attrs and set operators")
246 for k in set(self.legacy_schema).intersection(self.data)
247 ]
248
249 def validate(self):
250 keys = set(self.data)
251 if 'attrs' in keys and keys.intersection(self.compat_conversion):
252 raise PolicyValidationError(
253 "flow-log filter doesn't allow combining legacy keys with list-item attrs")
254 return super().validate()
255
256 def convert(self):
257 self.source_data = {}
258 # no mixing of legacy and list-item style
259 if 'attrs' in self.data:
260 return
261 data = {}
262 if self.data.get('enabled', False):
263 data['count_op'] = 'gte'
264 data['count'] = 1
265 else:
266 data['count'] = 0
267 attrs = []
268 for k in self.compat_conversion:
269 if k not in self.data:
270 continue
271 afilter = {}
272 cinfo = self.compat_conversion[k]
273 ak = cinfo['key']
274 av = self.data[k]
275 if 'values' in cinfo:
276 av = cinfo['values'][av]
277 if 'op' in self.data and self.data['op'] == 'not-equal':
278 av = {'value': av, 'op': 'not-equal'}
279 afilter[ak] = av
280 attrs.append(afilter)
281 if attrs:
282 data['attrs'] = attrs
283 data['type'] = self.type
284 self.source_data = self.data
285 self.data = data
286
287 def get_item_values(self, resource):
288 flogs = self.flow_log_map.get(resource[self.manager.resource_type.id], ())
289 # compatibility with v1 filter, we also add list-item annotation
290 # for matched flow logs
291 resource['c7n:flow-logs'] = flogs
292
293 # set operators are a little odd, but for list-item do require
294 # some runtime modification to ensure compatiblity.
295 if self.source_data.get('set-op', 'or') == 'and':
296 self.data['count'] = len(flogs)
297 return flogs
298
299 def process(self, resources, event=None):
300 self.convert()
301 self.flow_log_map = {}
302 for r in self.manager.get_resource_manager('flow-log').resources():
303 self.flow_log_map.setdefault(r['ResourceId'], []).append(r)
304 return super().process(resources, event)
305
306
307@Vpc.filter_registry.register('security-group')
308class VpcSecurityGroupFilter(RelatedResourceFilter):
309 """Filter VPCs based on Security Group attributes
310
311 :example:
312
313 .. code-block:: yaml
314
315 policies:
316 - name: vpc-by-sg
317 resource: vpc
318 filters:
319 - type: security-group
320 key: tag:Color
321 value: Gray
322 """
323 schema = type_schema(
324 'security-group', rinherit=ValueFilter.schema,
325 **{'match-resource': {'type': 'boolean'},
326 'operator': {'enum': ['and', 'or']}})
327 RelatedResource = "c7n.resources.vpc.SecurityGroup"
328 RelatedIdsExpression = '[SecurityGroups][].GroupId'
329 AnnotationKey = "matched-vpcs"
330
331 def get_related_ids(self, resources):
332 vpc_ids = [vpc['VpcId'] for vpc in resources]
333 vpc_group_ids = {
334 g['GroupId'] for g in
335 self.manager.get_resource_manager('security-group').resources()
336 if g.get('VpcId', '') in vpc_ids
337 }
338 return vpc_group_ids
339
340
341@Vpc.filter_registry.register('subnet')
342class VpcSubnetFilter(RelatedResourceFilter):
343 """Filter VPCs based on Subnet attributes
344
345 :example:
346
347 .. code-block:: yaml
348
349 policies:
350 - name: vpc-by-subnet
351 resource: vpc
352 filters:
353 - type: subnet
354 key: tag:Color
355 value: Gray
356 """
357 schema = type_schema(
358 'subnet', rinherit=ValueFilter.schema,
359 **{'match-resource': {'type': 'boolean'},
360 'operator': {'enum': ['and', 'or']}})
361 RelatedResource = "c7n.resources.vpc.Subnet"
362 RelatedIdsExpression = '[Subnets][].SubnetId'
363 AnnotationKey = "MatchedVpcsSubnets"
364
365 def get_related_ids(self, resources):
366 vpc_ids = [vpc['VpcId'] for vpc in resources]
367 vpc_subnet_ids = {
368 g['SubnetId'] for g in
369 self.manager.get_resource_manager('subnet').resources()
370 if g.get('VpcId', '') in vpc_ids
371 }
372 return vpc_subnet_ids
373
374
375@Vpc.filter_registry.register('nat-gateway')
376class VpcNatGatewayFilter(RelatedResourceFilter):
377 """Filter VPCs based on NAT Gateway attributes
378
379 :example:
380
381 .. code-block:: yaml
382
383 policies:
384 - name: vpc-by-nat
385 resource: vpc
386 filters:
387 - type: nat-gateway
388 key: tag:Color
389 value: Gray
390 """
391 schema = type_schema(
392 'nat-gateway', rinherit=ValueFilter.schema,
393 **{'match-resource': {'type': 'boolean'},
394 'operator': {'enum': ['and', 'or']}})
395 RelatedResource = "c7n.resources.vpc.NATGateway"
396 RelatedIdsExpression = '[NatGateways][].NatGatewayId'
397 AnnotationKey = "MatchedVpcsNatGateways"
398
399 def get_related_ids(self, resources):
400 vpc_ids = [vpc['VpcId'] for vpc in resources]
401 vpc_natgw_ids = {
402 g['NatGatewayId'] for g in
403 self.manager.get_resource_manager('nat-gateway').resources()
404 if g.get('VpcId', '') in vpc_ids
405 }
406 return vpc_natgw_ids
407
408
409@Vpc.filter_registry.register('internet-gateway')
410class VpcInternetGatewayFilter(RelatedResourceFilter):
411 """Filter VPCs based on Internet Gateway attributes
412
413 :example:
414
415 .. code-block:: yaml
416
417 policies:
418 - name: vpc-by-igw
419 resource: vpc
420 filters:
421 - type: internet-gateway
422 key: tag:Color
423 value: Gray
424 """
425 schema = type_schema(
426 'internet-gateway', rinherit=ValueFilter.schema,
427 **{'match-resource': {'type': 'boolean'},
428 'operator': {'enum': ['and', 'or']}})
429 RelatedResource = "c7n.resources.vpc.InternetGateway"
430 RelatedIdsExpression = '[InternetGateways][].InternetGatewayId'
431 AnnotationKey = "MatchedVpcsIgws"
432
433 def get_related_ids(self, resources):
434 vpc_ids = [vpc['VpcId'] for vpc in resources]
435 vpc_igw_ids = set()
436 for igw in self.manager.get_resource_manager('internet-gateway').resources():
437 for attachment in igw['Attachments']:
438 if attachment.get('VpcId', '') in vpc_ids:
439 vpc_igw_ids.add(igw['InternetGatewayId'])
440 return vpc_igw_ids
441
442
443@Vpc.filter_registry.register('vpc-attributes')
444class AttributesFilter(Filter):
445 """Filters VPCs based on their DNS attributes
446
447 :example:
448
449 .. code-block:: yaml
450
451 policies:
452 - name: dns-hostname-enabled
453 resource: vpc
454 filters:
455 - type: vpc-attributes
456 dnshostnames: True
457 """
458 schema = type_schema(
459 'vpc-attributes',
460 dnshostnames={'type': 'boolean'},
461 addressusage={'type': 'boolean'},
462 dnssupport={'type': 'boolean'})
463
464 permissions = ('ec2:DescribeVpcAttribute',)
465
466 key_params = (
467 ('dnshostnames', 'enableDnsHostnames'),
468 ('dnssupport', 'enableDnsSupport'),
469 ('addressusage', 'enableNetworkAddressUsageMetrics')
470 )
471 annotation_key = 'c7n:attributes'
472
473 def process(self, resources, event=None):
474 results = []
475 client = local_session(self.manager.session_factory).client('ec2')
476
477 for r in resources:
478 found = True
479 for policy_key, vpc_attr in self.key_params:
480 if policy_key not in self.data:
481 continue
482 policy_value = self.data[policy_key]
483 response_attr = "%s%s" % (vpc_attr[0].upper(), vpc_attr[1:])
484 value = client.describe_vpc_attribute(
485 VpcId=r['VpcId'],
486 Attribute=vpc_attr
487 )
488 value = value[response_attr]['Value']
489 r.setdefault(self.annotation_key, {})[policy_key] = value
490 if policy_value != value:
491 found = False
492 break
493 if found:
494 results.append(r)
495 return results
496
497
498@Vpc.filter_registry.register('dhcp-options')
499class DhcpOptionsFilter(Filter):
500 """Filter VPCs based on their dhcp options
501
502 :example:
503
504 .. code-block:: yaml
505
506 policies:
507 - name: vpcs-in-domain
508 resource: vpc
509 filters:
510 - type: dhcp-options
511 domain-name: ec2.internal
512
513 if an option value is specified as a list, then all elements must be present.
514 if an option value is specified as a string, then that string must be present.
515
516 vpcs not matching a given option value can be found via specifying
517 a `present: false` parameter.
518
519 """
520
521 option_keys = ('domain-name', 'domain-name-servers', 'ntp-servers')
522 schema = type_schema('dhcp-options', **{
523 k: {'oneOf': [
524 {'type': 'array', 'items': {'type': 'string'}},
525 {'type': 'string'}]}
526 for k in option_keys})
527 schema['properties']['present'] = {'type': 'boolean'}
528 permissions = ('ec2:DescribeDhcpOptions',)
529
530 def validate(self):
531 if not any([self.data.get(k) for k in self.option_keys]):
532 raise PolicyValidationError("one of %s required" % (self.option_keys,))
533 return self
534
535 def process(self, resources, event=None):
536 client = local_session(self.manager.session_factory).client('ec2')
537 option_ids = [r['DhcpOptionsId'] for r in resources]
538 options_map = {}
539 results = []
540 for options in client.describe_dhcp_options(
541 Filters=[{
542 'Name': 'dhcp-options-id',
543 'Values': option_ids}]).get('DhcpOptions', ()):
544 options_map[options['DhcpOptionsId']] = {
545 o['Key']: [v['Value'] for v in o['Values']]
546 for o in options['DhcpConfigurations']}
547
548 for vpc in resources:
549 if self.process_vpc(vpc, options_map[vpc['DhcpOptionsId']]):
550 results.append(vpc)
551 return results
552
553 def process_vpc(self, vpc, dhcp):
554 vpc['c7n:DhcpConfiguration'] = dhcp
555 found = True
556 for k in self.option_keys:
557 if k not in self.data:
558 continue
559 is_list = isinstance(self.data[k], list)
560 if k not in dhcp:
561 found = False
562 elif not is_list and self.data[k] not in dhcp[k]:
563 found = False
564 elif is_list and sorted(self.data[k]) != sorted(dhcp[k]):
565 found = False
566 if not self.data.get('present', True):
567 found = not found
568 return found
569
570
571@Vpc.action_registry.register('post-finding')
572class VpcPostFinding(PostFinding):
573
574 resource_type = "AwsEc2Vpc"
575
576 def format_resource(self, r):
577 envelope, payload = self.format_envelope(r)
578 # more inane sechub formatting deltas
579 detail = {
580 'DhcpOptionsId': r.get('DhcpOptionsId'),
581 'State': r['State']}
582
583 for assoc in r.get('CidrBlockAssociationSet', ()):
584 detail.setdefault('CidrBlockAssociationSet', []).append(dict(
585 AssociationId=assoc['AssociationId'],
586 CidrBlock=assoc['CidrBlock'],
587 CidrBlockState=assoc['CidrBlockState']['State']))
588
589 for assoc in r.get('Ipv6CidrBlockAssociationSet', ()):
590 detail.setdefault('Ipv6CidrBlockAssociationSet', []).append(dict(
591 AssociationId=assoc['AssociationId'],
592 Ipv6CidrBlock=assoc['Ipv6CidrBlock'],
593 CidrBlockState=assoc['Ipv6CidrBlockState']['State']))
594 payload.update(self.filter_empty(detail))
595 return envelope
596
597
598class DescribeSubnets(query.DescribeSource):
599
600 def get_resources(self, resource_ids):
601 while resource_ids:
602 try:
603 return super().get_resources(resource_ids)
604 except ClientError as e:
605 if e.response['Error']['Code'] != 'InvalidSubnetID.NotFound':
606 raise
607 sid = extract_subnet_id(e)
608 if sid:
609 resource_ids.remove(sid)
610 else:
611 return []
612
613
614RE_ERROR_SUBNET_ID = re.compile("'(?P<subnet_id>subnet-.*?)'")
615
616
617def extract_subnet_id(state_error):
618 "Extract an subnet id from an error"
619 subnet_id = None
620 match = RE_ERROR_SUBNET_ID.search(str(state_error))
621 if match:
622 subnet_id = match.groupdict().get('subnet_id')
623 return subnet_id
624
625
626@resources.register('subnet')
627class Subnet(query.QueryResourceManager):
628
629 class resource_type(query.TypeInfo):
630 service = 'ec2'
631 arn_type = 'subnet'
632 enum_spec = ('describe_subnets', 'Subnets', None)
633 name = id = 'SubnetId'
634 filter_name = 'SubnetIds'
635 filter_type = 'list'
636 cfn_type = config_type = 'AWS::EC2::Subnet'
637 id_prefix = "subnet-"
638
639 source_mapping = {
640 'describe': DescribeSubnets,
641 'config': query.ConfigSource}
642
643
644Subnet.filter_registry.register('flow-logs', FlowLogv2Filter)
645
646
647@Subnet.filter_registry.register('vpc')
648class SubnetVpcFilter(net_filters.VpcFilter):
649
650 RelatedIdsExpression = "VpcId"
651
652
653@Subnet.filter_registry.register('ip-address-usage')
654class SubnetIpAddressUsageFilter(ValueFilter):
655 """Filter subnets based on available IP addresses.
656
657 :example:
658
659 Show subnets with no addresses in use.
660
661 .. code-block:: yaml
662
663 policies:
664 - name: empty-subnets
665 resource: aws.subnet
666 filters:
667 - type: ip-address-usage
668 key: NumberUsed
669 value: 0
670
671 :example:
672
673 Show subnets where 90% or more addresses are in use.
674
675 .. code-block:: yaml
676
677 policies:
678 - name: almost-full-subnets
679 resource: aws.subnet
680 filters:
681 - type: ip-address-usage
682 key: PercentUsed
683 op: greater-than
684 value: 90
685
686 This filter allows ``key`` to be:
687
688 * ``MaxAvailable``: the number of addresses available based on a subnet's CIDR block size
689 (minus the 5 addresses
690 `reserved by AWS <https://docs.aws.amazon.com/vpc/latest/userguide/subnet-sizing.html>`_)
691 * ``NumberUsed``: ``MaxAvailable`` minus the subnet's ``AvailableIpAddressCount`` value
692 * ``PercentUsed``: ``NumberUsed`` divided by ``MaxAvailable``
693 """
694 annotation_key = 'c7n:IpAddressUsage'
695 aws_reserved_addresses = 5
696 schema_alias = False
697 schema = type_schema(
698 'ip-address-usage',
699 key={'enum': ['MaxAvailable', 'NumberUsed', 'PercentUsed']},
700 rinherit=ValueFilter.schema,
701 )
702
703 def augment(self, resource):
704 cidr_block = parse_cidr(resource['CidrBlock'])
705 max_addresses = cidr_block.num_addresses - self.aws_reserved_addresses
706 resource[self.annotation_key] = dict(
707 MaxAvailable=max_addresses,
708 NumberUsed=max_addresses - resource['AvailableIpAddressCount'],
709 PercentUsed=round(
710 (max_addresses - resource['AvailableIpAddressCount']) / max_addresses * 100.0,
711 2
712 ),
713 )
714
715 def process(self, resources, event=None):
716 results = []
717 for r in resources:
718 if self.annotation_key not in r:
719 self.augment(r)
720 if self.match(r[self.annotation_key]):
721 results.append(r)
722 return results
723
724
725class ConfigSG(query.ConfigSource):
726
727 def load_resource(self, item):
728 r = super(ConfigSG, self).load_resource(item)
729 for rset in ('IpPermissions', 'IpPermissionsEgress'):
730 for p in r.get(rset, ()):
731 if p.get('FromPort', '') is None:
732 p.pop('FromPort')
733 if p.get('ToPort', '') is None:
734 p.pop('ToPort')
735 if 'Ipv6Ranges' not in p:
736 p[u'Ipv6Ranges'] = []
737 for i in p.get('UserIdGroupPairs', ()):
738 for k, v in list(i.items()):
739 if v is None:
740 i.pop(k)
741 # legacy config form, still version 1.2
742 for attribute, element_key in (('IpRanges', u'CidrIp'),):
743 if attribute not in p:
744 continue
745 p[attribute] = [{element_key: v} for v in p[attribute]]
746 if 'Ipv4Ranges' in p:
747 p['IpRanges'] = p.pop('Ipv4Ranges')
748 return r
749
750
751@resources.register('security-group')
752class SecurityGroup(query.QueryResourceManager):
753
754 class resource_type(query.TypeInfo):
755 service = 'ec2'
756 arn_type = 'security-group'
757 enum_spec = ('describe_security_groups', 'SecurityGroups', None)
758 id = 'GroupId'
759 name = 'GroupName'
760 filter_name = "GroupIds"
761 filter_type = 'list'
762 cfn_type = config_type = "AWS::EC2::SecurityGroup"
763 id_prefix = "sg-"
764
765 source_mapping = {
766 'config': ConfigSG,
767 'describe': query.DescribeSource
768 }
769
770
771@SecurityGroup.filter_registry.register('diff')
772class SecurityGroupDiffFilter(Diff):
773
774 def diff(self, source, target):
775 differ = SecurityGroupDiff()
776 return differ.diff(source, target)
777
778
779class SecurityGroupDiff:
780 """Diff two versions of a security group
781
782 Immutable: GroupId, GroupName, Description, VpcId, OwnerId
783 Mutable: Tags, Rules
784 """
785
786 def diff(self, source, target):
787 delta = {}
788 tag_delta = self.get_tag_delta(source, target)
789 if tag_delta:
790 delta['tags'] = tag_delta
791 ingress_delta = self.get_rule_delta('IpPermissions', source, target)
792 if ingress_delta:
793 delta['ingress'] = ingress_delta
794 egress_delta = self.get_rule_delta(
795 'IpPermissionsEgress', source, target)
796 if egress_delta:
797 delta['egress'] = egress_delta
798 if delta:
799 return delta
800
801 def get_tag_delta(self, source, target):
802 source_tags = {t['Key']: t['Value'] for t in source.get('Tags', ())}
803 target_tags = {t['Key']: t['Value'] for t in target.get('Tags', ())}
804 target_keys = set(target_tags.keys())
805 source_keys = set(source_tags.keys())
806 removed = source_keys.difference(target_keys)
807 added = target_keys.difference(source_keys)
808 changed = set()
809 for k in target_keys.intersection(source_keys):
810 if source_tags[k] != target_tags[k]:
811 changed.add(k)
812 return {k: v for k, v in {
813 'added': {k: target_tags[k] for k in added},
814 'removed': {k: source_tags[k] for k in removed},
815 'updated': {k: target_tags[k] for k in changed}}.items() if v}
816
817 def get_rule_delta(self, key, source, target):
818 source_rules = {
819 self.compute_rule_hash(r): r for r in source.get(key, ())}
820 target_rules = {
821 self.compute_rule_hash(r): r for r in target.get(key, ())}
822 source_keys = set(source_rules.keys())
823 target_keys = set(target_rules.keys())
824 removed = source_keys.difference(target_keys)
825 added = target_keys.difference(source_keys)
826 return {k: v for k, v in
827 {'removed': [source_rules[rid] for rid in sorted(removed)],
828 'added': [target_rules[rid] for rid in sorted(added)]}.items() if v}
829
830 RULE_ATTRS = (
831 ('PrefixListIds', 'PrefixListId'),
832 ('UserIdGroupPairs', 'GroupId'),
833 ('IpRanges', 'CidrIp'),
834 ('Ipv6Ranges', 'CidrIpv6')
835 )
836
837 def compute_rule_hash(self, rule):
838 buf = "%d-%d-%s-" % (
839 rule.get('FromPort', 0) or 0,
840 rule.get('ToPort', 0) or 0,
841 rule.get('IpProtocol', '-1') or '-1'
842 )
843 for a, ke in self.RULE_ATTRS:
844 if a not in rule:
845 continue
846 ev = [e[ke] for e in rule[a]]
847 ev.sort()
848 for e in ev:
849 buf += "%s-" % e
850 # mask to generate the same numeric value across all Python versions
851 return zlib.crc32(buf.encode('ascii')) & 0xffffffff
852
853
854@SecurityGroup.action_registry.register('patch')
855class SecurityGroupApplyPatch(BaseAction):
856 """Modify a resource via application of a reverse delta.
857 """
858 schema = type_schema('patch')
859
860 permissions = ('ec2:AuthorizeSecurityGroupIngress',
861 'ec2:AuthorizeSecurityGroupEgress',
862 'ec2:RevokeSecurityGroupIngress',
863 'ec2:RevokeSecurityGroupEgress',
864 'ec2:CreateTags',
865 'ec2:DeleteTags')
866
867 def validate(self):
868 diff_filters = [n for n in self.manager.iter_filters() if isinstance(
869 n, SecurityGroupDiffFilter)]
870 if not len(diff_filters):
871 raise PolicyValidationError(
872 "resource patching requires diff filter")
873 return self
874
875 def process(self, resources):
876 client = local_session(self.manager.session_factory).client('ec2')
877 differ = SecurityGroupDiff()
878 patcher = SecurityGroupPatch()
879 for r in resources:
880 # reverse the patch by computing fresh, the forward
881 # patch is for notifications
882 d = differ.diff(r, r['c7n:previous-revision']['resource'])
883 patcher.apply_delta(client, r, d)
884
885
886class SecurityGroupPatch:
887
888 RULE_TYPE_MAP = {
889 'egress': ('IpPermissionsEgress',
890 'revoke_security_group_egress',
891 'authorize_security_group_egress'),
892 'ingress': ('IpPermissions',
893 'revoke_security_group_ingress',
894 'authorize_security_group_ingress')}
895
896 retry = staticmethod(get_retry((
897 'RequestLimitExceeded', 'Client.RequestLimitExceeded')))
898
899 def apply_delta(self, client, target, change_set):
900 if 'tags' in change_set:
901 self.process_tags(client, target, change_set['tags'])
902 if 'ingress' in change_set:
903 self.process_rules(
904 client, 'ingress', target, change_set['ingress'])
905 if 'egress' in change_set:
906 self.process_rules(
907 client, 'egress', target, change_set['egress'])
908
909 def process_tags(self, client, group, tag_delta):
910 if 'removed' in tag_delta:
911 self.retry(client.delete_tags,
912 Resources=[group['GroupId']],
913 Tags=[{'Key': k}
914 for k in tag_delta['removed']])
915 tags = []
916 if 'added' in tag_delta:
917 tags.extend(
918 [{'Key': k, 'Value': v}
919 for k, v in tag_delta['added'].items()])
920 if 'updated' in tag_delta:
921 tags.extend(
922 [{'Key': k, 'Value': v}
923 for k, v in tag_delta['updated'].items()])
924 if tags:
925 self.retry(
926 client.create_tags, Resources=[group['GroupId']], Tags=tags)
927
928 def process_rules(self, client, rule_type, group, delta):
929 _, revoke_op, auth_op = self.RULE_TYPE_MAP[rule_type]
930 revoke, authorize = getattr(
931 client, revoke_op), getattr(client, auth_op)
932
933 # Process removes
934 if 'removed' in delta:
935 self.retry(revoke, GroupId=group['GroupId'],
936 IpPermissions=[r for r in delta['removed']])
937
938 # Process adds
939 if 'added' in delta:
940 self.retry(authorize, GroupId=group['GroupId'],
941 IpPermissions=[r for r in delta['added']])
942
943
944class SGUsage(Filter):
945
946 nics = ()
947
948 def get_permissions(self):
949 return list(itertools.chain(
950 *[self.manager.get_resource_manager(m).get_permissions()
951 for m in
952 ['lambda', 'eni', 'launch-config', 'security-group', 'event-rule-target',
953 'aws.batch-compute']]))
954
955 def filter_peered_refs(self, resources):
956 if not resources:
957 return resources
958 # Check that groups are not referenced across accounts
959 client = local_session(self.manager.session_factory).client('ec2')
960 peered_ids = set()
961 for resource_set in chunks(resources, 200):
962 for sg_ref in client.describe_security_group_references(
963 GroupId=[r['GroupId'] for r in resource_set]
964 )['SecurityGroupReferenceSet']:
965 peered_ids.add(sg_ref['GroupId'])
966 self.log.debug(
967 "%d of %d groups w/ peered refs", len(peered_ids), len(resources))
968 return [r for r in resources if r['GroupId'] not in peered_ids]
969
970 def get_scanners(self):
971 return (
972 ("nics", self.get_eni_sgs),
973 ("sg-perm-refs", self.get_sg_refs),
974 ('lambdas', self.get_lambda_sgs),
975 ("launch-configs", self.get_launch_config_sgs),
976 ("ecs-cwe", self.get_ecs_cwe_sgs),
977 ("codebuild", self.get_codebuild_sgs),
978 ("batch", self.get_batch_sgs),
979 )
980
981 def scan_groups(self):
982 used = set()
983 for kind, scanner in self.get_scanners():
984 sg_ids = scanner()
985 new_refs = sg_ids.difference(used)
986 used = used.union(sg_ids)
987 self.log.debug(
988 "%s using %d sgs, new refs %s total %s",
989 kind, len(sg_ids), len(new_refs), len(used))
990
991 return used
992
993 def get_launch_config_sgs(self):
994 # Note assuming we also have launch config garbage collection
995 # enabled.
996 sg_ids = set()
997 for cfg in self.manager.get_resource_manager('launch-config').resources():
998 for g in cfg['SecurityGroups']:
999 sg_ids.add(g)
1000 for g in cfg['ClassicLinkVPCSecurityGroups']:
1001 sg_ids.add(g)
1002 return sg_ids
1003
1004 def get_lambda_sgs(self):
1005 sg_ids = set()
1006 for func in self.manager.get_resource_manager('lambda').resources(augment=False):
1007 if 'VpcConfig' not in func:
1008 continue
1009 for g in func['VpcConfig']['SecurityGroupIds']:
1010 sg_ids.add(g)
1011 return sg_ids
1012
1013 def get_eni_sgs(self):
1014 sg_ids = set()
1015 self.nics = self.manager.get_resource_manager('eni').resources()
1016 for nic in self.nics:
1017 for g in nic['Groups']:
1018 sg_ids.add(g['GroupId'])
1019 return sg_ids
1020
1021 def get_codebuild_sgs(self):
1022 sg_ids = set()
1023 for cb in self.manager.get_resource_manager('codebuild').resources():
1024 sg_ids |= set(cb.get('vpcConfig', {}).get('securityGroupIds', []))
1025 return sg_ids
1026
1027 def get_sg_refs(self):
1028 sg_ids = set()
1029 for sg in self.manager.get_resource_manager('security-group').resources():
1030 for perm_type in ('IpPermissions', 'IpPermissionsEgress'):
1031 for p in sg.get(perm_type, []):
1032 for g in p.get('UserIdGroupPairs', ()):
1033 # self references aren't usage.
1034 if g['GroupId'] != sg['GroupId']:
1035 sg_ids.add(g['GroupId'])
1036 return sg_ids
1037
1038 def get_ecs_cwe_sgs(self):
1039 sg_ids = set()
1040 expr = jmespath_compile(
1041 'EcsParameters.NetworkConfiguration.awsvpcConfiguration.SecurityGroups[]')
1042 for rule in self.manager.get_resource_manager(
1043 'event-rule-target').resources(augment=False):
1044 ids = expr.search(rule)
1045 if ids:
1046 sg_ids.update(ids)
1047 return sg_ids
1048
1049 def get_batch_sgs(self):
1050 expr = jmespath_compile('[].computeResources.securityGroupIds[]')
1051 resources = self.manager.get_resource_manager('aws.batch-compute').resources(augment=False)
1052 return set(expr.search(resources) or [])
1053
1054
1055@SecurityGroup.filter_registry.register('unused')
1056class UnusedSecurityGroup(SGUsage):
1057 """Filter to just vpc security groups that are not used.
1058
1059 We scan all extant enis in the vpc to get a baseline set of groups
1060 in use. Then augment with those referenced by launch configs, and
1061 lambdas as they may not have extant resources in the vpc at a
1062 given moment. We also find any security group with references from
1063 other security group either within the vpc or across peered
1064 connections. Also checks cloud watch event targeting ecs.
1065
1066 Checks - enis, lambda, launch-configs, sg rule refs, and ecs cwe
1067 targets.
1068
1069 Note this filter does not support classic security groups atm.
1070
1071 :example:
1072
1073 .. code-block:: yaml
1074
1075 policies:
1076 - name: security-groups-unused
1077 resource: security-group
1078 filters:
1079 - unused
1080
1081 """
1082 schema = type_schema('unused')
1083
1084 def process(self, resources, event=None):
1085 used = self.scan_groups()
1086 unused = [
1087 r for r in resources
1088 if r['GroupId'] not in used and 'VpcId' in r]
1089 return unused and self.filter_peered_refs(unused) or []
1090
1091
1092@SecurityGroup.filter_registry.register('used')
1093class UsedSecurityGroup(SGUsage):
1094 """Filter to security groups that are used.
1095 This operates as a complement to the unused filter for multi-step
1096 workflows.
1097
1098 :example:
1099
1100 .. code-block:: yaml
1101
1102 policies:
1103 - name: security-groups-in-use
1104 resource: security-group
1105 filters:
1106 - used
1107
1108 policies:
1109 - name: security-groups-used-by-rds
1110 resource: security-group
1111 filters:
1112 - used
1113 - type: value
1114 key: c7n:InstanceOwnerIds
1115 op: intersect
1116 value:
1117 - amazon-rds
1118
1119 policies:
1120 - name: security-groups-used-by-natgw
1121 resource: security-group
1122 filters:
1123 - used
1124 - type: value
1125 key: c7n:InterfaceTypes
1126 op: intersect
1127 value:
1128 - nat_gateway
1129
1130 policies:
1131 - name: security-groups-used-by-alb
1132 resource: security-group
1133 filters:
1134 - used
1135 - type: value
1136 key: c7n:InterfaceResourceTypes
1137 op: intersect
1138 value:
1139 - elb-app
1140 """
1141 schema = type_schema('used')
1142
1143 instance_owner_id_key = 'c7n:InstanceOwnerIds'
1144 interface_type_key = 'c7n:InterfaceTypes'
1145 interface_resource_type_key = 'c7n:InterfaceResourceTypes'
1146
1147 def _get_eni_attributes(self):
1148 group_enis = {}
1149 for nic in self.nics:
1150 instance_owner_id, interface_resource_type = '', ''
1151 if nic['Status'] == 'in-use':
1152 if nic.get('Attachment') and 'InstanceOwnerId' in nic['Attachment']:
1153 instance_owner_id = nic['Attachment']['InstanceOwnerId']
1154 interface_resource_type = get_eni_resource_type(nic)
1155 interface_type = nic.get('InterfaceType')
1156 for g in nic['Groups']:
1157 group_enis.setdefault(g['GroupId'], []).append({
1158 'InstanceOwnerId': instance_owner_id,
1159 'InterfaceType': interface_type,
1160 'InterfaceResourceType': interface_resource_type
1161 })
1162 return group_enis
1163
1164 def process(self, resources, event=None):
1165 used = self.scan_groups()
1166 unused = [
1167 r for r in resources
1168 if r['GroupId'] not in used and 'VpcId' in r]
1169 unused = {g['GroupId'] for g in self.filter_peered_refs(unused)}
1170 group_enis = self._get_eni_attributes()
1171 for r in resources:
1172 enis = group_enis.get(r['GroupId'], ())
1173 r[self.instance_owner_id_key] = list({
1174 i['InstanceOwnerId'] for i in enis if i['InstanceOwnerId']})
1175 r[self.interface_type_key] = list({
1176 i['InterfaceType'] for i in enis if i['InterfaceType']})
1177 r[self.interface_resource_type_key] = list({
1178 i['InterfaceResourceType'] for i in enis if i['InterfaceResourceType']})
1179 return [r for r in resources if r['GroupId'] not in unused]
1180
1181
1182@SecurityGroup.filter_registry.register('stale')
1183class Stale(Filter):
1184 """Filter to find security groups that contain stale references
1185 to other groups that are either no longer present or traverse
1186 a broken vpc peering connection. Note this applies to VPC
1187 Security groups only and will implicitly filter security groups.
1188
1189 AWS Docs:
1190 https://docs.aws.amazon.com/vpc/latest/peering/vpc-peering-security-groups.html
1191
1192 :example:
1193
1194 .. code-block:: yaml
1195
1196 policies:
1197 - name: stale-security-groups
1198 resource: security-group
1199 filters:
1200 - stale
1201 """
1202 schema = type_schema('stale')
1203 permissions = ('ec2:DescribeStaleSecurityGroups',)
1204
1205 def process(self, resources, event=None):
1206 client = local_session(self.manager.session_factory).client('ec2')
1207 vpc_ids = {r['VpcId'] for r in resources if 'VpcId' in r}
1208 group_map = {r['GroupId']: r for r in resources}
1209 results = []
1210 self.log.debug("Querying %d vpc for stale refs", len(vpc_ids))
1211 stale_count = 0
1212 for vpc_id in vpc_ids:
1213 stale_groups = client.describe_stale_security_groups(
1214 VpcId=vpc_id).get('StaleSecurityGroupSet', ())
1215
1216 stale_count += len(stale_groups)
1217 for s in stale_groups:
1218 if s['GroupId'] in group_map:
1219 r = group_map[s['GroupId']]
1220 if 'StaleIpPermissions' in s:
1221 r['MatchedIpPermissions'] = s['StaleIpPermissions']
1222 if 'StaleIpPermissionsEgress' in s:
1223 r['MatchedIpPermissionsEgress'] = s[
1224 'StaleIpPermissionsEgress']
1225 results.append(r)
1226 self.log.debug("Found %d stale security groups", stale_count)
1227 return results
1228
1229
1230@SecurityGroup.filter_registry.register('default-vpc')
1231class SGDefaultVpc(net_filters.DefaultVpcBase):
1232 """Filter that returns any security group that exists within the default vpc
1233
1234 :example:
1235
1236 .. code-block:: yaml
1237
1238 policies:
1239 - name: security-group-default-vpc
1240 resource: security-group
1241 filters:
1242 - default-vpc
1243 """
1244
1245 schema = type_schema('default-vpc')
1246
1247 def __call__(self, resource, event=None):
1248 if 'VpcId' not in resource:
1249 return False
1250 return self.match(resource['VpcId'])
1251
1252
1253class SGPermission(Filter):
1254 """Filter for verifying security group ingress and egress permissions
1255
1256 All attributes of a security group permission are available as
1257 value filters.
1258
1259 If multiple attributes are specified the permission must satisfy
1260 all of them. Note that within an attribute match against a list value
1261 of a permission we default to or.
1262
1263 If a group has any permissions that match all conditions, then it
1264 matches the filter.
1265
1266 Permissions that match on the group are annotated onto the group and
1267 can subsequently be used by the remove-permission action.
1268
1269 We have specialized handling for matching `Ports` in ingress/egress
1270 permission From/To range. The following example matches on ingress
1271 rules which allow for a range that includes all of the given ports.
1272
1273 .. code-block:: yaml
1274
1275 - type: ingress
1276 Ports: [22, 443, 80]
1277
1278 As well for verifying that a rule only allows for a specific set of ports
1279 as in the following example. The delta between this and the previous
1280 example is that if the permission allows for any ports not specified here,
1281 then the rule will match. ie. OnlyPorts is a negative assertion match,
1282 it matches when a permission includes ports outside of the specified set.
1283
1284 .. code-block:: yaml
1285
1286 - type: ingress
1287 OnlyPorts: [22]
1288
1289 For simplifying ipranges handling which is specified as a list on a rule
1290 we provide a `Cidr` key which can be used as a value type filter evaluated
1291 against each of the rules. If any iprange cidr match then the permission
1292 matches.
1293
1294 .. code-block:: yaml
1295
1296 - type: ingress
1297 IpProtocol: -1
1298 FromPort: 445
1299
1300 We also have specialized handling for matching self-references in
1301 ingress/egress permissions. The following example matches on ingress
1302 rules which allow traffic its own same security group.
1303
1304 .. code-block:: yaml
1305
1306 - type: ingress
1307 SelfReference: True
1308
1309 As well for assertions that a ingress/egress permission only matches
1310 a given set of ports, *note* OnlyPorts is an inverse match.
1311
1312 .. code-block:: yaml
1313
1314 - type: egress
1315 OnlyPorts: [22, 443, 80]
1316
1317 - type: egress
1318 Cidr:
1319 value_type: cidr
1320 op: in
1321 value: x.y.z
1322
1323 `value_type: cidr` can also filter if cidr is a subset of cidr
1324 value range. In this example we are allowing any smaller cidrs within
1325 allowed_cidrs.csv.
1326
1327 .. code-block:: yaml
1328
1329 - type: ingress
1330 Cidr:
1331 value_type: cidr
1332 op: not-in
1333 value_from:
1334 url: s3://a-policy-data-us-west-2/allowed_cidrs.csv
1335 format: csv
1336
1337 or value can be specified as a list.
1338
1339 .. code-block:: yaml
1340
1341 - type: ingress
1342 Cidr:
1343 value_type: cidr
1344 op: not-in
1345 value: ["10.0.0.0/8", "192.168.0.0/16"]
1346
1347 `Cidr` can match ipv4 rules and `CidrV6` can match ipv6 rules. In
1348 this example we are blocking global inbound connections to SSH or
1349 RDP.
1350
1351 .. code-block:: yaml
1352
1353 - or:
1354 - type: ingress
1355 Ports: [22, 3389]
1356 Cidr:
1357 value: "0.0.0.0/0"
1358 - type: ingress
1359 Ports: [22, 3389]
1360 CidrV6:
1361 value: "::/0"
1362
1363 `SGReferences` can be used to filter out SG references in rules.
1364 In this example we want to block ingress rules that reference a SG
1365 that is tagged with `Access: Public`.
1366
1367 .. code-block:: yaml
1368
1369 - type: ingress
1370 SGReferences:
1371 key: "tag:Access"
1372 value: "Public"
1373 op: equal
1374
1375 We can also filter SG references based on the VPC that they are
1376 within. In this example we want to ensure that our outbound rules
1377 that reference SGs are only referencing security groups within a
1378 specified VPC.
1379
1380 .. code-block:: yaml
1381
1382 - type: egress
1383 SGReferences:
1384 key: 'VpcId'
1385 value: 'vpc-11a1a1aa'
1386 op: equal
1387
1388 Likewise, we can also filter SG references by their description.
1389 For example, we can prevent egress rules from referencing any
1390 SGs that have a description of "default - DO NOT USE".
1391
1392 .. code-block:: yaml
1393
1394 - type: egress
1395 SGReferences:
1396 key: 'Description'
1397 value: 'default - DO NOT USE'
1398 op: equal
1399
1400 By default, this filter matches a security group rule if
1401 _all_ of its keys match. Using `match-operator: or` causes a match
1402 if _any_ key matches. This can help consolidate some simple
1403 cases that would otherwise require multiple filters. To find
1404 security groups that allow all inbound traffic over IPv4 or IPv6,
1405 for example, we can use two filters inside an `or` block:
1406
1407 .. code-block:: yaml
1408
1409 - or:
1410 - type: ingress
1411 Cidr: "0.0.0.0/0"
1412 - type: ingress
1413 CidrV6: "::/0"
1414
1415 or combine them into a single filter:
1416
1417 .. code-block:: yaml
1418
1419 - type: ingress
1420 match-operator: or
1421 Cidr: "0.0.0.0/0"
1422 CidrV6: "::/0"
1423
1424 Note that evaluating _combinations_ of factors (e.g. traffic over
1425 port 22 from 0.0.0.0/0) still requires separate filters.
1426 """
1427
1428 perm_attrs = {
1429 'IpProtocol', 'FromPort', 'ToPort', 'UserIdGroupPairs',
1430 'IpRanges', 'PrefixListIds'}
1431 filter_attrs = {
1432 'Cidr', 'CidrV6', 'Ports', 'OnlyPorts',
1433 'SelfReference', 'Description', 'SGReferences'}
1434 attrs = perm_attrs.union(filter_attrs)
1435 attrs.add('match-operator')
1436 attrs.add('match-operator')
1437
1438 def validate(self):
1439 delta = set(self.data.keys()).difference(self.attrs)
1440 delta.remove('type')
1441 if delta:
1442 raise PolicyValidationError("Unknown keys %s on %s" % (
1443 ", ".join(delta), self.manager.data))
1444 return self
1445
1446 def process(self, resources, event=None):
1447 self.vfilters = []
1448 fattrs = list(sorted(self.perm_attrs.intersection(self.data.keys())))
1449 self.ports = 'Ports' in self.data and self.data['Ports'] or ()
1450 self.only_ports = (
1451 'OnlyPorts' in self.data and self.data['OnlyPorts'] or ())
1452 for f in fattrs:
1453 fv = self.data.get(f)
1454 if isinstance(fv, dict):
1455 fv['key'] = f
1456 else:
1457 fv = {f: fv}
1458 vf = ValueFilter(fv, self.manager)
1459 vf.annotate = False
1460 self.vfilters.append(vf)
1461 return super(SGPermission, self).process(resources, event)
1462
1463 def process_ports(self, perm):
1464 found = None
1465 if 'FromPort' in perm and 'ToPort' in perm:
1466 for port in self.ports:
1467 if port >= perm['FromPort'] and port <= perm['ToPort']:
1468 found = True
1469 break
1470 found = False
1471 only_found = False
1472 for port in self.only_ports:
1473 if port == perm['FromPort'] and port == perm['ToPort']:
1474 only_found = True
1475 if self.only_ports and not only_found:
1476 found = found is None or found and True or False
1477 if self.only_ports and only_found:
1478 found = False
1479 return found
1480
1481 def _process_cidr(self, cidr_key, cidr_type, range_type, perm):
1482
1483 found = None
1484 ip_perms = perm.get(range_type, [])
1485 if not ip_perms:
1486 return False
1487
1488 match_range = self.data[cidr_key]
1489
1490 if isinstance(match_range, dict):
1491 match_range['key'] = cidr_type
1492 else:
1493 match_range = {cidr_type: match_range}
1494
1495 vf = ValueFilter(match_range, self.manager)
1496 vf.annotate = False
1497
1498 for ip_range in ip_perms:
1499 found = vf(ip_range)
1500 if found:
1501 break
1502 else:
1503 found = False
1504 return found
1505
1506 def process_cidrs(self, perm):
1507 found_v6 = found_v4 = None
1508 if 'CidrV6' in self.data:
1509 found_v6 = self._process_cidr('CidrV6', 'CidrIpv6', 'Ipv6Ranges', perm)
1510 if 'Cidr' in self.data:
1511 found_v4 = self._process_cidr('Cidr', 'CidrIp', 'IpRanges', perm)
1512 match_op = self.data.get('match-operator', 'and') == 'and' and all or any
1513 cidr_match = [k for k in (found_v6, found_v4) if k is not None]
1514 if not cidr_match:
1515 return None
1516 return match_op(cidr_match)
1517
1518 def process_description(self, perm):
1519 if 'Description' not in self.data:
1520 return None
1521
1522 d = dict(self.data['Description'])
1523 d['key'] = 'Description'
1524
1525 vf = ValueFilter(d, self.manager)
1526 vf.annotate = False
1527
1528 for k in ('Ipv6Ranges', 'IpRanges', 'UserIdGroupPairs', 'PrefixListIds'):
1529 if k not in perm or not perm[k]:
1530 continue
1531 return vf(perm[k][0])
1532 return False
1533
1534 def process_self_reference(self, perm, sg_id):
1535 found = None
1536 ref_match = self.data.get('SelfReference')
1537 if ref_match is not None:
1538 found = False
1539 if 'UserIdGroupPairs' in perm and 'SelfReference' in self.data:
1540 self_reference = sg_id in [p['GroupId']
1541 for p in perm['UserIdGroupPairs']]
1542 if ref_match is False and not self_reference:
1543 found = True
1544 if ref_match is True and self_reference:
1545 found = True
1546 return found
1547
1548 def process_sg_references(self, perm, owner_id):
1549 sg_refs = self.data.get('SGReferences')
1550 if not sg_refs:
1551 return None
1552
1553 sg_perm = perm.get('UserIdGroupPairs', [])
1554 if not sg_perm:
1555 return False
1556
1557 sg_group_ids = [p['GroupId'] for p in sg_perm if p.get('UserId', '') == owner_id]
1558 sg_resources = self.manager.get_resources(sg_group_ids)
1559 vf = ValueFilter(sg_refs, self.manager)
1560 vf.annotate = False
1561
1562 for sg in sg_resources:
1563 if vf(sg):
1564 return True
1565 return False
1566
1567 def expand_permissions(self, permissions):
1568 """Expand each list of cidr, prefix list, user id group pair
1569 by port/protocol as an individual rule.
1570
1571 The console ux automatically expands them out as addition/removal is
1572 per this expansion, the describe calls automatically group them.
1573 """
1574 for p in permissions:
1575 np = dict(p)
1576 values = {}
1577 for k in (u'IpRanges',
1578 u'Ipv6Ranges',
1579 u'PrefixListIds',
1580 u'UserIdGroupPairs'):
1581 values[k] = np.pop(k, ())
1582 np[k] = []
1583 for k, v in values.items():
1584 if not v:
1585 continue
1586 for e in v:
1587 ep = dict(np)
1588 ep[k] = [e]
1589 yield ep
1590
1591 def __call__(self, resource):
1592 matched = []
1593 sg_id = resource['GroupId']
1594 owner_id = resource['OwnerId']
1595 match_op = self.data.get('match-operator', 'and') == 'and' and all or any
1596 for perm in self.expand_permissions(resource[self.ip_permissions_key]):
1597 perm_matches = {}
1598 for idx, f in enumerate(self.vfilters):
1599 perm_matches[idx] = bool(f(perm))
1600 perm_matches['description'] = self.process_description(perm)
1601 perm_matches['ports'] = self.process_ports(perm)
1602 perm_matches['cidrs'] = self.process_cidrs(perm)
1603 perm_matches['self-refs'] = self.process_self_reference(perm, sg_id)
1604 perm_matches['sg-refs'] = self.process_sg_references(perm, owner_id)
1605 perm_match_values = list(filter(
1606 lambda x: x is not None, perm_matches.values()))
1607
1608 # account for one python behavior any([]) == False, all([]) == True
1609 if match_op == all and not perm_match_values:
1610 continue
1611
1612 match = match_op(perm_match_values)
1613 if match:
1614 matched.append(perm)
1615
1616 if matched:
1617 matched_annotation = resource.setdefault('Matched%s' % self.ip_permissions_key, [])
1618 # If the same rule matches multiple filters, only add it to the match annotation
1619 # once. Note: Because we're looking for unique dicts and those aren't hashable,
1620 # we can't conveniently use set() to de-duplicate rules.
1621 matched_annotation.extend(m for m in matched if m not in matched_annotation)
1622 return True
1623
1624
1625SGPermissionSchema = {
1626 'match-operator': {'type': 'string', 'enum': ['or', 'and']},
1627 'Ports': {'type': 'array', 'items': {'type': 'integer'}},
1628 'SelfReference': {'type': 'boolean'},
1629 'OnlyPorts': {'type': 'array', 'items': {'type': 'integer'}},
1630 'IpProtocol': {
1631 'oneOf': [
1632 {'enum': ["-1", -1, 'tcp', 'udp', 'icmp', 'icmpv6']},
1633 {'$ref': '#/definitions/filters/value'}
1634 ]
1635 },
1636 'FromPort': {'oneOf': [
1637 {'$ref': '#/definitions/filters/value'},
1638 {'type': 'integer'}]},
1639 'ToPort': {'oneOf': [
1640 {'$ref': '#/definitions/filters/value'},
1641 {'type': 'integer'}]},
1642 'UserIdGroupPairs': {},
1643 'IpRanges': {},
1644 'PrefixListIds': {},
1645 'Description': {},
1646 'Cidr': {},
1647 'CidrV6': {},
1648 'SGReferences': {}
1649}
1650
1651
1652@SecurityGroup.filter_registry.register('ingress')
1653class IPPermission(SGPermission):
1654
1655 ip_permissions_key = "IpPermissions"
1656 schema = {
1657 'type': 'object',
1658 'additionalProperties': False,
1659 'properties': {'type': {'enum': ['ingress']}},
1660 'required': ['type']}
1661 schema['properties'].update(SGPermissionSchema)
1662
1663
1664@SecurityGroup.filter_registry.register('egress')
1665class IPPermissionEgress(SGPermission):
1666
1667 ip_permissions_key = "IpPermissionsEgress"
1668 schema = {
1669 'type': 'object',
1670 'additionalProperties': False,
1671 'properties': {'type': {'enum': ['egress']}},
1672 'required': ['type']}
1673 schema['properties'].update(SGPermissionSchema)
1674
1675
1676@SecurityGroup.action_registry.register('delete')
1677class Delete(BaseAction):
1678 """Action to delete security group(s)
1679
1680 It is recommended to apply a filter to the delete policy to avoid the
1681 deletion of all security groups returned.
1682
1683 :example:
1684
1685 .. code-block:: yaml
1686
1687 policies:
1688 - name: security-groups-unused-delete
1689 resource: security-group
1690 filters:
1691 - type: unused
1692 actions:
1693 - delete
1694 """
1695
1696 schema = type_schema('delete')
1697 permissions = ('ec2:DeleteSecurityGroup',)
1698
1699 def process(self, resources):
1700 client = local_session(self.manager.session_factory).client('ec2')
1701 for r in resources:
1702 client.delete_security_group(GroupId=r['GroupId'])
1703
1704
1705@SecurityGroup.action_registry.register('remove-permissions')
1706class RemovePermissions(BaseAction):
1707 """Action to remove ingress/egress rule(s) from a security group
1708
1709 :example:
1710
1711 .. code-block:: yaml
1712
1713 policies:
1714 - name: security-group-revoke-8080
1715 resource: security-group
1716 filters:
1717 - type: ingress
1718 IpProtocol: tcp
1719 Ports: [8080]
1720 actions:
1721 - type: remove-permissions
1722 ingress: matched
1723
1724 """
1725 schema = type_schema(
1726 'remove-permissions',
1727 ingress={'type': 'string', 'enum': ['matched', 'all']},
1728 egress={'type': 'string', 'enum': ['matched', 'all']})
1729
1730 permissions = ('ec2:RevokeSecurityGroupIngress',
1731 'ec2:RevokeSecurityGroupEgress')
1732
1733 def process(self, resources):
1734 i_perms = self.data.get('ingress', 'matched')
1735 e_perms = self.data.get('egress', 'matched')
1736
1737 client = local_session(self.manager.session_factory).client('ec2')
1738 for r in resources:
1739 for label, perms in [('ingress', i_perms), ('egress', e_perms)]:
1740 if perms == 'matched':
1741 key = 'MatchedIpPermissions%s' % (
1742 label == 'egress' and 'Egress' or '')
1743 groups = r.get(key, ())
1744 elif perms == 'all':
1745 key = 'IpPermissions%s' % (
1746 label == 'egress' and 'Egress' or '')
1747 groups = r.get(key, ())
1748 elif isinstance(perms, list):
1749 groups = perms
1750 else:
1751 continue
1752 if not groups:
1753 continue
1754 method = getattr(client, 'revoke_security_group_%s' % label)
1755 method(GroupId=r['GroupId'], IpPermissions=groups)
1756
1757
1758@SecurityGroup.action_registry.register('set-permissions')
1759class SetPermissions(BaseAction):
1760 """Action to add/remove ingress/egress rule(s) to a security group
1761
1762 :example:
1763
1764 .. code-block:: yaml
1765
1766 policies:
1767 - name: ops-access-via
1768 resource: aws.security-group
1769 filters:
1770 - type: ingress
1771 IpProtocol: "-1"
1772 Ports: [22, 3389]
1773 Cidr: "0.0.0.0/0"
1774 actions:
1775 - type: set-permissions
1776 # remove the permission matched by a previous ingress filter.
1777 remove-ingress: matched
1778 # remove permissions by specifying them fully, ie remove default outbound
1779 # access.
1780 remove-egress:
1781 - IpProtocol: "-1"
1782 Cidr: "0.0.0.0/0"
1783
1784 # add a list of permissions to the group.
1785 add-ingress:
1786 # full syntax/parameters to authorize can be used.
1787 - IpPermissions:
1788 - IpProtocol: TCP
1789 FromPort: 22
1790 ToPort: 22
1791 IpRanges:
1792 - Description: Ops SSH Access
1793 CidrIp: "1.1.1.1/32"
1794 - Description: Security SSH Access
1795 CidrIp: "2.2.2.2/32"
1796 # add a list of egress permissions to a security group
1797 add-egress:
1798 - IpProtocol: "TCP"
1799 FromPort: 5044
1800 ToPort: 5044
1801 CidrIp: "192.168.1.2/32"
1802
1803 """
1804 schema = type_schema(
1805 'set-permissions',
1806 **{'add-ingress': {'type': 'array', 'items': {'type': 'object', 'minProperties': 1}},
1807 'remove-ingress': {'oneOf': [
1808 {'enum': ['all', 'matched']},
1809 {'type': 'array', 'items': {'type': 'object', 'minProperties': 2}}]},
1810 'add-egress': {'type': 'array', 'items': {'type': 'object', 'minProperties': 1}},
1811 'remove-egress': {'oneOf': [
1812 {'enum': ['all', 'matched']},
1813 {'type': 'array', 'items': {'type': 'object', 'minProperties': 2}}]}}
1814 )
1815 permissions = (
1816 'ec2:AuthorizeSecurityGroupEgress',
1817 'ec2:AuthorizeSecurityGroupIngress',)
1818
1819 ingress_shape = "AuthorizeSecurityGroupIngressRequest"
1820 egress_shape = "AuthorizeSecurityGroupEgressRequest"
1821
1822 def validate(self):
1823 request_template = {'GroupId': 'sg-06bc5ce18a2e5d57a'}
1824 for perm_type, shape in (
1825 ('egress', self.egress_shape), ('ingress', self.ingress_shape)):
1826 for perm in self.data.get('add-%s' % type, ()):
1827 params = dict(request_template)
1828 params.update(perm)
1829 shape_validate(params, shape, 'ec2')
1830
1831 def get_permissions(self):
1832 perms = ()
1833 if 'add-ingress' in self.data:
1834 perms += ('ec2:AuthorizeSecurityGroupIngress',)
1835 if 'add-egress' in self.data:
1836 perms += ('ec2:AuthorizeSecurityGroupEgress',)
1837 if 'remove-ingress' in self.data or 'remove-egress' in self.data:
1838 perms += RemovePermissions.permissions
1839 if not perms:
1840 perms = self.permissions + RemovePermissions.permissions
1841 return perms
1842
1843 def process(self, resources):
1844 client = local_session(self.manager.session_factory).client('ec2')
1845 for r in resources:
1846 for method, permissions in (
1847 (client.authorize_security_group_egress, self.data.get('add-egress', ())),
1848 (client.authorize_security_group_ingress, self.data.get('add-ingress', ()))):
1849 for p in permissions:
1850 p = dict(p)
1851 p['GroupId'] = r['GroupId']
1852 try:
1853 method(**p)
1854 except ClientError as e:
1855 if e.response['Error']['Code'] != 'InvalidPermission.Duplicate':
1856 raise
1857
1858 remover = RemovePermissions(
1859 {'ingress': self.data.get('remove-ingress', ()),
1860 'egress': self.data.get('remove-egress', ())}, self.manager)
1861 remover.process(resources)
1862
1863
1864@SecurityGroup.action_registry.register('post-finding')
1865class SecurityGroupPostFinding(OtherResourcePostFinding):
1866
1867 def format_resource(self, r):
1868 fr = super(SecurityGroupPostFinding, self).format_resource(r)
1869 fr['Type'] = 'AwsEc2SecurityGroup'
1870 return fr
1871
1872
1873class DescribeENI(query.DescribeSource):
1874
1875 def augment(self, resources):
1876 for r in resources:
1877 r['Tags'] = r.pop('TagSet', [])
1878 return resources
1879
1880
1881@resources.register('eni')
1882class NetworkInterface(query.QueryResourceManager):
1883
1884 class resource_type(query.TypeInfo):
1885 service = 'ec2'
1886 arn_type = 'network-interface'
1887 enum_spec = ('describe_network_interfaces', 'NetworkInterfaces', None)
1888 name = id = 'NetworkInterfaceId'
1889 filter_name = 'NetworkInterfaceIds'
1890 filter_type = 'list'
1891 cfn_type = config_type = "AWS::EC2::NetworkInterface"
1892 id_prefix = "eni-"
1893
1894 source_mapping = {
1895 'describe': DescribeENI,
1896 'config': query.ConfigSource
1897 }
1898
1899
1900NetworkInterface.filter_registry.register('flow-logs', FlowLogv2Filter)
1901NetworkInterface.filter_registry.register(
1902 'network-location', net_filters.NetworkLocation)
1903
1904
1905@NetworkInterface.filter_registry.register('subnet')
1906class InterfaceSubnetFilter(net_filters.SubnetFilter):
1907 """Network interface subnet filter
1908
1909 :example:
1910
1911 .. code-block:: yaml
1912
1913 policies:
1914 - name: network-interface-in-subnet
1915 resource: eni
1916 filters:
1917 - type: subnet
1918 key: CidrBlock
1919 value: 10.0.2.0/24
1920 """
1921
1922 RelatedIdsExpression = "SubnetId"
1923
1924
1925@NetworkInterface.filter_registry.register('security-group')
1926class InterfaceSecurityGroupFilter(net_filters.SecurityGroupFilter):
1927 """Network interface security group filter
1928
1929 :example:
1930
1931 .. code-block:: yaml
1932
1933 policies:
1934 - name: network-interface-ssh
1935 resource: eni
1936 filters:
1937 - type: security-group
1938 match-resource: true
1939 key: FromPort
1940 value: 22
1941 """
1942
1943 RelatedIdsExpression = "Groups[].GroupId"
1944
1945
1946@NetworkInterface.filter_registry.register('vpc')
1947class InterfaceVpcFilter(net_filters.VpcFilter):
1948
1949 RelatedIdsExpression = "VpcId"
1950
1951
1952@NetworkInterface.action_registry.register('modify-security-groups')
1953class InterfaceModifyVpcSecurityGroups(ModifyVpcSecurityGroupsAction):
1954 """Remove security groups from an interface.
1955
1956 Can target either physical groups as a list of group ids or
1957 symbolic groups like 'matched' or 'all'. 'matched' uses
1958 the annotations of the 'group' interface filter.
1959
1960 Note an interface always gets at least one security group, so
1961 we also allow specification of an isolation/quarantine group
1962 that can be specified if there would otherwise be no groups.
1963
1964
1965 :example:
1966
1967 .. code-block:: yaml
1968
1969 policies:
1970 - name: network-interface-remove-group
1971 resource: eni
1972 filters:
1973 - type: security-group
1974 match-resource: true
1975 key: FromPort
1976 value: 22
1977 actions:
1978 - type: modify-security-groups
1979 isolation-group: sg-01ab23c4
1980 add: []
1981 """
1982 permissions = ('ec2:ModifyNetworkInterfaceAttribute',)
1983
1984 def process(self, resources):
1985 client = local_session(self.manager.session_factory).client('ec2')
1986 groups = super(
1987 InterfaceModifyVpcSecurityGroups, self).get_groups(resources)
1988 for idx, r in enumerate(resources):
1989 client.modify_network_interface_attribute(
1990 NetworkInterfaceId=r['NetworkInterfaceId'],
1991 Groups=groups[idx])
1992
1993
1994@NetworkInterface.action_registry.register('delete')
1995class DeleteNetworkInterface(BaseAction):
1996 """Delete a network interface.
1997
1998 :example:
1999
2000 .. code-block:: yaml
2001
2002 policies:
2003 - name: mark-orphaned-enis
2004 comment: Flag abandoned Lambda VPC ENIs for deletion
2005 resource: eni
2006 filters:
2007 - Status: available
2008 - type: value
2009 op: glob
2010 key: Description
2011 value: "AWS Lambda VPC ENI*"
2012 - "tag:custodian_status": absent
2013 actions:
2014 - type: mark-for-op
2015 tag: custodian_status
2016 msg: "Orphaned Lambda VPC ENI: {op}@{action_date}"
2017 op: delete
2018 days: 1
2019
2020 - name: delete-marked-enis
2021 comment: Delete flagged ENIs that have not been cleaned up naturally
2022 resource: eni
2023 filters:
2024 - type: marked-for-op
2025 tag: custodian_status
2026 op: delete
2027 actions:
2028 - type: delete
2029 """
2030 permissions = ('ec2:DeleteNetworkInterface',)
2031 schema = type_schema('delete')
2032
2033 def process(self, resources):
2034 client = local_session(self.manager.session_factory).client('ec2')
2035 for r in resources:
2036 try:
2037 self.manager.retry(
2038 client.delete_network_interface,
2039 NetworkInterfaceId=r['NetworkInterfaceId'])
2040 except ClientError as err:
2041 if not err.response['Error']['Code'] == 'InvalidNetworkInterfaceID.NotFound':
2042 raise
2043
2044
2045@NetworkInterface.action_registry.register('detach')
2046class DetachNetworkInterface(BaseAction):
2047 """Detach a network interface from an EC2 instance.
2048
2049 :example:
2050
2051 .. code-block:: yaml
2052
2053 policies:
2054 - name: detach-enis
2055 comment: Detach ENIs attached to EC2 with public IP addresses
2056 resource: eni
2057 filters:
2058 - type: value
2059 key: Attachment.InstanceId
2060 value: present
2061 - type: value
2062 key: Association.PublicIp
2063 value: present
2064 actions:
2065 - type: detach
2066 """
2067 permissions = ('ec2:DetachNetworkInterface',)
2068 schema = type_schema('detach')
2069
2070 def process(self, resources):
2071 client = local_session(self.manager.session_factory).client('ec2')
2072 att_resources = [ar for ar in resources if ('Attachment' in ar
2073 and ar['Attachment'].get('InstanceId')
2074 and ar['Attachment'].get('DeviceIndex') != 0)]
2075 if att_resources and (len(att_resources) < len(resources)):
2076 self.log.warning(
2077 "Filtered {} of {} non-primary network interfaces attatched to EC2".format(
2078 len(att_resources), len(resources))
2079 )
2080 elif not att_resources:
2081 self.log.warning("No non-primary EC2 interfaces indentified - revise c7n filters")
2082 for r in att_resources:
2083 client.detach_network_interface(AttachmentId=r['Attachment']['AttachmentId'])
2084
2085
2086@resources.register('route-table')
2087class RouteTable(query.QueryResourceManager):
2088
2089 class resource_type(query.TypeInfo):
2090 service = 'ec2'
2091 arn_type = 'route-table'
2092 enum_spec = ('describe_route_tables', 'RouteTables', None)
2093 name = id = 'RouteTableId'
2094 filter_name = 'RouteTableIds'
2095 filter_type = 'list'
2096 id_prefix = "rtb-"
2097 cfn_type = config_type = "AWS::EC2::RouteTable"
2098
2099
2100@RouteTable.filter_registry.register('vpc')
2101class RouteTableVpcFilter(net_filters.VpcFilter):
2102
2103 RelatedIdsExpression = "VpcId"
2104
2105
2106@RouteTable.filter_registry.register('subnet')
2107class SubnetRoute(net_filters.SubnetFilter):
2108 """Filter a route table by its associated subnet attributes."""
2109
2110 RelatedIdsExpression = "Associations[].SubnetId"
2111
2112 RelatedMapping = None
2113
2114 def get_related_ids(self, resources):
2115 if self.RelatedIdMapping is None:
2116 return super(SubnetRoute, self).get_related_ids(resources)
2117 return list(itertools.chain(*[self.RelatedIdMapping[r['RouteTableId']] for r in resources]))
2118
2119 def get_related(self, resources):
2120 rt_subnet_map = {}
2121 main_tables = {}
2122
2123 manager = self.get_resource_manager()
2124 for r in resources:
2125 rt_subnet_map[r['RouteTableId']] = []
2126 for a in r.get('Associations', ()):
2127 if 'SubnetId' in a:
2128 rt_subnet_map[r['RouteTableId']].append(a['SubnetId'])
2129 elif a.get('Main'):
2130 main_tables[r['VpcId']] = r['RouteTableId']
2131 explicit_subnet_ids = set(itertools.chain(*rt_subnet_map.values()))
2132 subnets = manager.resources()
2133 for s in subnets:
2134 if s['SubnetId'] in explicit_subnet_ids:
2135 continue
2136 if s['VpcId'] not in main_tables:
2137 continue
2138 rt_subnet_map.setdefault(main_tables[s['VpcId']], []).append(s['SubnetId'])
2139 related_subnets = set(itertools.chain(*rt_subnet_map.values()))
2140 self.RelatedIdMapping = rt_subnet_map
2141 return {s['SubnetId']: s for s in subnets if s['SubnetId'] in related_subnets}
2142
2143
2144@RouteTable.filter_registry.register('route')
2145class Route(ValueFilter):
2146 """Filter a route table by its routes' attributes."""
2147
2148 schema = type_schema('route', rinherit=ValueFilter.schema)
2149 schema_alias = False
2150
2151 def process(self, resources, event=None):
2152 results = []
2153 for r in resources:
2154 matched = []
2155 for route in r['Routes']:
2156 if self.match(route):
2157 matched.append(route)
2158 if matched:
2159 r.setdefault('c7n:matched-routes', []).extend(matched)
2160 results.append(r)
2161 return results
2162
2163
2164@resources.register('transit-gateway')
2165class TransitGateway(query.QueryResourceManager):
2166
2167 class resource_type(query.TypeInfo):
2168 service = 'ec2'
2169 enum_spec = ('describe_transit_gateways', 'TransitGateways', None)
2170 name = id = 'TransitGatewayId'
2171 arn = "TransitGatewayArn"
2172 id_prefix = "tgw-"
2173 filter_name = 'TransitGatewayIds'
2174 filter_type = 'list'
2175 config_type = cfn_type = 'AWS::EC2::TransitGateway'
2176
2177
2178TransitGateway.filter_registry.register('flow-logs', FlowLogv2Filter)
2179
2180
2181class TransitGatewayAttachmentQuery(query.ChildResourceQuery):
2182
2183 def get_parent_parameters(self, params, parent_id, parent_key):
2184 merged_params = dict(params)
2185 merged_params.setdefault('Filters', []).append(
2186 {'Name': parent_key, 'Values': [parent_id]})
2187 return merged_params
2188
2189
2190@query.sources.register('transit-attachment')
2191class TransitAttachmentSource(query.ChildDescribeSource):
2192
2193 resource_query_factory = TransitGatewayAttachmentQuery
2194
2195
2196@resources.register('transit-attachment')
2197class TransitGatewayAttachment(query.ChildResourceManager):
2198
2199 child_source = 'transit-attachment'
2200
2201 class resource_type(query.TypeInfo):
2202 service = 'ec2'
2203 enum_spec = ('describe_transit_gateway_attachments', 'TransitGatewayAttachments', None)
2204 parent_spec = ('transit-gateway', 'transit-gateway-id', None)
2205 id_prefix = 'tgw-attach-'
2206 name = id = 'TransitGatewayAttachmentId'
2207 metrics_namespace = 'AWS/TransitGateway'
2208 arn = False
2209 cfn_type = 'AWS::EC2::TransitGatewayAttachment'
2210 supports_trailevents = True
2211
2212
2213@TransitGatewayAttachment.filter_registry.register('metrics')
2214class TransitGatewayAttachmentMetricsFilter(MetricsFilter):
2215
2216 def get_dimensions(self, resource):
2217 return [
2218 {'Name': 'TransitGateway', 'Value': resource['TransitGatewayId']},
2219 {'Name': 'TransitGatewayAttachment', 'Value': resource['TransitGatewayAttachmentId']}
2220 ]
2221
2222
2223@resources.register('peering-connection')
2224class PeeringConnection(query.QueryResourceManager):
2225
2226 class resource_type(query.TypeInfo):
2227 service = 'ec2'
2228 arn_type = 'vpc-peering-connection'
2229 enum_spec = ('describe_vpc_peering_connections',
2230 'VpcPeeringConnections', None)
2231 name = id = 'VpcPeeringConnectionId'
2232 filter_name = 'VpcPeeringConnectionIds'
2233 filter_type = 'list'
2234 id_prefix = "pcx-"
2235 cfn_type = config_type = "AWS::EC2::VPCPeeringConnection"
2236
2237
2238@PeeringConnection.filter_registry.register('cross-account')
2239class CrossAccountPeer(CrossAccountAccessFilter):
2240
2241 schema = type_schema(
2242 'cross-account',
2243 # white list accounts
2244 whitelist_from=resolver.ValuesFrom.schema,
2245 whitelist={'type': 'array', 'items': {'type': 'string'}})
2246
2247 permissions = ('ec2:DescribeVpcPeeringConnections',)
2248
2249 def process(self, resources, event=None):
2250 results = []
2251 accounts = self.get_accounts()
2252 owners = map(jmespath_compile, (
2253 'AccepterVpcInfo.OwnerId', 'RequesterVpcInfo.OwnerId'))
2254
2255 for r in resources:
2256 for o_expr in owners:
2257 account_id = o_expr.search(r)
2258 if account_id and account_id not in accounts:
2259 r.setdefault(
2260 'c7n:CrossAccountViolations', []).append(account_id)
2261 results.append(r)
2262 return results
2263
2264
2265@PeeringConnection.filter_registry.register('missing-route')
2266class MissingRoute(Filter):
2267 """Return peers which are missing a route in route tables.
2268
2269 If the peering connection is between two vpcs in the same account,
2270 the connection is returned unless it is in present route tables in
2271 each vpc.
2272
2273 If the peering connection is between accounts, then the local vpc's
2274 route table is checked.
2275 """
2276
2277 schema = type_schema('missing-route')
2278 permissions = ('ec2:DescribeRouteTables',)
2279
2280 def process(self, resources, event=None):
2281 tables = self.manager.get_resource_manager(
2282 'route-table').resources()
2283 routed_vpcs = {}
2284 mid = 'VpcPeeringConnectionId'
2285 for t in tables:
2286 for r in t.get('Routes', ()):
2287 if mid in r:
2288 routed_vpcs.setdefault(r[mid], []).append(t['VpcId'])
2289 results = []
2290 for r in resources:
2291 if r[mid] not in routed_vpcs:
2292 results.append(r)
2293 continue
2294 for k in ('AccepterVpcInfo', 'RequesterVpcInfo'):
2295 if r[k]['OwnerId'] != self.manager.config.account_id:
2296 continue
2297 if r[k].get('Region') and r['k']['Region'] != self.manager.config.region:
2298 continue
2299 if r[k]['VpcId'] not in routed_vpcs[r['VpcPeeringConnectionId']]:
2300 results.append(r)
2301 break
2302 return results
2303
2304
2305@resources.register('network-acl')
2306class NetworkAcl(query.QueryResourceManager):
2307
2308 class resource_type(query.TypeInfo):
2309 service = 'ec2'
2310 arn_type = 'network-acl'
2311 enum_spec = ('describe_network_acls', 'NetworkAcls', None)
2312 name = id = 'NetworkAclId'
2313 filter_name = 'NetworkAclIds'
2314 filter_type = 'list'
2315 cfn_type = config_type = "AWS::EC2::NetworkAcl"
2316 id_prefix = "acl-"
2317
2318
2319@NetworkAcl.filter_registry.register('subnet')
2320class AclSubnetFilter(net_filters.SubnetFilter):
2321 """Filter network acls by the attributes of their attached subnets.
2322
2323 :example:
2324
2325 .. code-block:: yaml
2326
2327 policies:
2328 - name: subnet-acl
2329 resource: network-acl
2330 filters:
2331 - type: subnet
2332 key: "tag:Location"
2333 value: Public
2334 """
2335
2336 RelatedIdsExpression = "Associations[].SubnetId"
2337
2338
2339@NetworkAcl.filter_registry.register('s3-cidr')
2340class AclAwsS3Cidrs(Filter):
2341 """Filter network acls by those that allow access to s3 cidrs.
2342
2343 Defaults to filtering those nacls that do not allow s3 communication.
2344
2345 :example:
2346
2347 Find all nacls that do not allow communication with s3.
2348
2349 .. code-block:: yaml
2350
2351 policies:
2352 - name: s3-not-allowed-nacl
2353 resource: network-acl
2354 filters:
2355 - s3-cidr
2356 """
2357 # TODO allow for port specification as range
2358 schema = type_schema(
2359 's3-cidr',
2360 egress={'type': 'boolean', 'default': True},
2361 ingress={'type': 'boolean', 'default': True},
2362 present={'type': 'boolean', 'default': False})
2363
2364 permissions = ('ec2:DescribePrefixLists',)
2365
2366 def process(self, resources, event=None):
2367 ec2 = local_session(self.manager.session_factory).client('ec2')
2368 cidrs = jmespath_search(
2369 "PrefixLists[].Cidrs[]", ec2.describe_prefix_lists())
2370 cidrs = [parse_cidr(cidr) for cidr in cidrs]
2371 results = []
2372
2373 check_egress = self.data.get('egress', True)
2374 check_ingress = self.data.get('ingress', True)
2375 present = self.data.get('present', False)
2376
2377 for r in resources:
2378 matched = {cidr: None for cidr in cidrs}
2379 for entry in r['Entries']:
2380 if entry['Egress'] and not check_egress:
2381 continue
2382 if not entry['Egress'] and not check_ingress:
2383 continue
2384 entry_cidr = parse_cidr(entry['CidrBlock'])
2385 for c in matched:
2386 if c in entry_cidr and matched[c] is None:
2387 matched[c] = (
2388 entry['RuleAction'] == 'allow' and True or False)
2389 if present and all(matched.values()):
2390 results.append(r)
2391 elif not present and not all(matched.values()):
2392 results.append(r)
2393 return results
2394
2395
2396class DescribeElasticIp(query.DescribeSource):
2397
2398 def augment(self, resources):
2399 return [r for r in resources if self.manager.resource_type.id in r]
2400
2401
2402@resources.register('elastic-ip', aliases=('network-addr',))
2403class NetworkAddress(query.QueryResourceManager):
2404
2405 class resource_type(query.TypeInfo):
2406 service = 'ec2'
2407 arn_type = 'elastic-ip'
2408 enum_spec = ('describe_addresses', 'Addresses', None)
2409 name = 'PublicIp'
2410 id = 'AllocationId'
2411 id_prefix = 'eipalloc-'
2412 filter_name = 'AllocationIds'
2413 filter_type = 'list'
2414 config_type = cfn_type = "AWS::EC2::EIP"
2415
2416 source_mapping = {
2417 'describe': DescribeElasticIp,
2418 'config': query.ConfigSource
2419 }
2420
2421
2422NetworkAddress.filter_registry.register('shield-enabled', IsEIPShieldProtected)
2423NetworkAddress.action_registry.register('set-shield', SetEIPShieldProtection)
2424
2425
2426@NetworkAddress.action_registry.register('release')
2427class AddressRelease(BaseAction):
2428 """Action to release elastic IP address(es)
2429
2430 Use the force option to cause any attached elastic IPs to
2431 also be released. Otherwise, only unattached elastic IPs
2432 will be released.
2433
2434 :example:
2435
2436 .. code-block:: yaml
2437
2438 policies:
2439 - name: release-network-addr
2440 resource: network-addr
2441 filters:
2442 - AllocationId: ...
2443 actions:
2444 - type: release
2445 force: True
2446 """
2447
2448 schema = type_schema('release', force={'type': 'boolean'})
2449 permissions = ('ec2:ReleaseAddress', 'ec2:DisassociateAddress',)
2450
2451 def process_attached(self, client, associated_addrs):
2452 for aa in list(associated_addrs):
2453 try:
2454 client.disassociate_address(AssociationId=aa['AssociationId'])
2455 except ClientError as e:
2456 # If its already been diassociated ignore, else raise.
2457 if not (e.response['Error']['Code'] == 'InvalidAssocationID.NotFound' and
2458 aa['AssocationId'] in e.response['Error']['Message']):
2459 raise e
2460 associated_addrs.remove(aa)
2461 return associated_addrs
2462
2463 def process(self, network_addrs):
2464 client = local_session(self.manager.session_factory).client('ec2')
2465 force = self.data.get('force')
2466 assoc_addrs = [addr for addr in network_addrs if 'AssociationId' in addr]
2467 unassoc_addrs = [addr for addr in network_addrs if 'AssociationId' not in addr]
2468
2469 if len(assoc_addrs) and not force:
2470 self.log.warning(
2471 "Filtered %d attached eips of %d eips. Use 'force: true' to release them.",
2472 len(assoc_addrs), len(network_addrs))
2473 elif len(assoc_addrs) and force:
2474 unassoc_addrs = itertools.chain(
2475 unassoc_addrs, self.process_attached(client, assoc_addrs))
2476
2477 for r in unassoc_addrs:
2478 try:
2479 client.release_address(AllocationId=r['AllocationId'])
2480 except ClientError as e:
2481 # If its already been released, ignore, else raise.
2482 if e.response['Error']['Code'] == 'InvalidAddress.PtrSet':
2483 self.log.warning(
2484 "EIP %s cannot be released because it has a PTR record set.",
2485 r['AllocationId'])
2486 if e.response['Error']['Code'] == 'InvalidAddress.Locked':
2487 self.log.warning(
2488 "EIP %s cannot be released because it is locked to your account.",
2489 r['AllocationId'])
2490 if e.response['Error']['Code'] != 'InvalidAllocationID.NotFound':
2491 raise
2492
2493
2494@NetworkAddress.action_registry.register('disassociate')
2495class DisassociateAddress(BaseAction):
2496 """Disassociate elastic IP addresses from resources without releasing them.
2497
2498 :example:
2499
2500 .. code-block:: yaml
2501
2502 policies:
2503 - name: disassociate-network-addr
2504 resource: network-addr
2505 filters:
2506 - AllocationId: ...
2507 actions:
2508 - type: disassociate
2509 """
2510
2511 schema = type_schema('disassociate')
2512 permissions = ('ec2:DisassociateAddress',)
2513
2514 def process(self, network_addrs):
2515 client = local_session(self.manager.session_factory).client('ec2')
2516 assoc_addrs = [addr for addr in network_addrs if 'AssociationId' in addr]
2517
2518 for aa in assoc_addrs:
2519 try:
2520 client.disassociate_address(AssociationId=aa['AssociationId'])
2521 except ClientError as e:
2522 # If its already been diassociated ignore, else raise.
2523 if not (e.response['Error']['Code'] == 'InvalidAssocationID.NotFound' and
2524 aa['AssocationId'] in e.response['Error']['Message']):
2525 raise e
2526
2527
2528@resources.register('customer-gateway')
2529class CustomerGateway(query.QueryResourceManager):
2530
2531 class resource_type(query.TypeInfo):
2532 service = 'ec2'
2533 arn_type = 'customer-gateway'
2534 enum_spec = ('describe_customer_gateways', 'CustomerGateways', None)
2535 id = 'CustomerGatewayId'
2536 filter_name = 'CustomerGatewayIds'
2537 filter_type = 'list'
2538 name = 'CustomerGatewayId'
2539 id_prefix = "cgw-"
2540 cfn_type = config_type = 'AWS::EC2::CustomerGateway'
2541
2542
2543@resources.register('internet-gateway')
2544class InternetGateway(query.QueryResourceManager):
2545
2546 class resource_type(query.TypeInfo):
2547 service = 'ec2'
2548 arn_type = 'internet-gateway'
2549 enum_spec = ('describe_internet_gateways', 'InternetGateways', None)
2550 name = id = 'InternetGatewayId'
2551 filter_name = 'InternetGatewayIds'
2552 filter_type = 'list'
2553 cfn_type = config_type = "AWS::EC2::InternetGateway"
2554 id_prefix = "igw-"
2555
2556
2557@InternetGateway.action_registry.register('delete')
2558class DeleteInternetGateway(BaseAction):
2559
2560 """Action to delete Internet Gateway
2561
2562 :example:
2563
2564 .. code-block:: yaml
2565
2566 policies:
2567 - name: delete-internet-gateway
2568 resource: internet-gateway
2569 actions:
2570 - type: delete
2571 """
2572
2573 schema = type_schema('delete')
2574 permissions = ('ec2:DeleteInternetGateway',)
2575
2576 def process(self, resources):
2577
2578 client = local_session(self.manager.session_factory).client('ec2')
2579 for r in resources:
2580 try:
2581 client.delete_internet_gateway(InternetGatewayId=r['InternetGatewayId'])
2582 except ClientError as err:
2583 if err.response['Error']['Code'] == 'DependencyViolation':
2584 self.log.warning(
2585 "%s error hit deleting internetgateway: %s",
2586 err.response['Error']['Code'],
2587 err.response['Error']['Message'],
2588 )
2589 elif err.response['Error']['Code'] == 'InvalidInternetGatewayId.NotFound':
2590 pass
2591 else:
2592 raise
2593
2594
2595@resources.register('nat-gateway')
2596class NATGateway(query.QueryResourceManager):
2597
2598 class resource_type(query.TypeInfo):
2599 service = 'ec2'
2600 arn_type = 'natgateway'
2601 enum_spec = ('describe_nat_gateways', 'NatGateways', None)
2602 name = id = 'NatGatewayId'
2603 filter_name = 'NatGatewayIds'
2604 filter_type = 'list'
2605 date = 'CreateTime'
2606 dimension = 'NatGatewayId'
2607 metrics_namespace = 'AWS/NATGateway'
2608 id_prefix = "nat-"
2609 cfn_type = config_type = 'AWS::EC2::NatGateway'
2610
2611
2612@NATGateway.action_registry.register('delete')
2613class DeleteNATGateway(BaseAction):
2614
2615 schema = type_schema('delete')
2616 permissions = ('ec2:DeleteNatGateway',)
2617
2618 def process(self, resources):
2619 client = local_session(self.manager.session_factory).client('ec2')
2620 for r in resources:
2621 client.delete_nat_gateway(NatGatewayId=r['NatGatewayId'])
2622
2623
2624@resources.register('vpn-connection')
2625class VPNConnection(query.QueryResourceManager):
2626
2627 class resource_type(query.TypeInfo):
2628 service = 'ec2'
2629 arn_type = 'vpn-connection'
2630 enum_spec = ('describe_vpn_connections', 'VpnConnections', None)
2631 name = id = 'VpnConnectionId'
2632 filter_name = 'VpnConnectionIds'
2633 filter_type = 'list'
2634 cfn_type = config_type = 'AWS::EC2::VPNConnection'
2635 id_prefix = "vpn-"
2636
2637
2638@resources.register('vpn-gateway')
2639class VPNGateway(query.QueryResourceManager):
2640
2641 class resource_type(query.TypeInfo):
2642 service = 'ec2'
2643 arn_type = 'vpn-gateway'
2644 enum_spec = ('describe_vpn_gateways', 'VpnGateways', None)
2645 name = id = 'VpnGatewayId'
2646 filter_name = 'VpnGatewayIds'
2647 filter_type = 'list'
2648 cfn_type = config_type = 'AWS::EC2::VPNGateway'
2649 id_prefix = "vgw-"
2650
2651
2652@resources.register('vpc-endpoint')
2653class VpcEndpoint(query.QueryResourceManager):
2654
2655 class resource_type(query.TypeInfo):
2656 service = 'ec2'
2657 arn_type = 'vpc-endpoint'
2658 enum_spec = ('describe_vpc_endpoints', 'VpcEndpoints', None)
2659 name = id = 'VpcEndpointId'
2660 metrics_namespace = "AWS/PrivateLinkEndpoints"
2661 date = 'CreationTimestamp'
2662 filter_name = 'VpcEndpointIds'
2663 filter_type = 'list'
2664 id_prefix = "vpce-"
2665 universal_taggable = object()
2666 cfn_type = config_type = "AWS::EC2::VPCEndpoint"
2667
2668
2669@VpcEndpoint.filter_registry.register('metrics')
2670class VpcEndpointMetricsFilter(MetricsFilter):
2671
2672 def get_dimensions(self, resource):
2673 return [
2674 {'Name': 'Endpoint Type', 'Value': resource['VpcEndpointType']},
2675 {'Name': 'Service Name', 'Value': resource['ServiceName']},
2676 {'Name': 'VPC Endpoint Id', 'Value': resource['VpcEndpointId']},
2677 {'Name': 'VPC Id', 'Value': resource['VpcId']},
2678 ]
2679
2680
2681@VpcEndpoint.filter_registry.register('has-statement')
2682class EndpointPolicyStatementFilter(HasStatementFilter):
2683 """Find resources with matching endpoint policy statements.
2684
2685 :example:
2686
2687 .. code-block:: yaml
2688
2689 policies:
2690 - name: vpc-endpoint-policy
2691 resource: aws.vpc-endpoint
2692 filters:
2693 - type: has-statement
2694 statements:
2695 - Action: "*"
2696 Effect: "Allow"
2697 """
2698
2699 policy_attribute = 'PolicyDocument'
2700 permissions = ('ec2:DescribeVpcEndpoints',)
2701
2702 def get_std_format_args(self, endpoint):
2703 return {
2704 'endpoint_id': endpoint['VpcEndpointId'],
2705 'account_id': self.manager.config.account_id,
2706 'region': self.manager.config.region
2707 }
2708
2709
2710@VpcEndpoint.filter_registry.register('cross-account')
2711class EndpointCrossAccountFilter(CrossAccountAccessFilter):
2712
2713 policy_attribute = 'PolicyDocument'
2714 annotation_key = 'c7n:CrossAccountViolations'
2715 permissions = ('ec2:DescribeVpcEndpoints',)
2716
2717
2718@VpcEndpoint.filter_registry.register('security-group')
2719class EndpointSecurityGroupFilter(net_filters.SecurityGroupFilter):
2720
2721 RelatedIdsExpression = "Groups[].GroupId"
2722
2723
2724@VpcEndpoint.filter_registry.register('subnet')
2725class EndpointSubnetFilter(net_filters.SubnetFilter):
2726
2727 RelatedIdsExpression = "SubnetIds[]"
2728
2729
2730@VpcEndpoint.filter_registry.register('vpc')
2731class EndpointVpcFilter(net_filters.VpcFilter):
2732
2733 RelatedIdsExpression = "VpcId"
2734
2735
2736@Vpc.filter_registry.register("vpc-endpoint")
2737class VPCEndpointFilter(RelatedResourceByIdFilter):
2738 """Filters vpcs based on their vpc-endpoints
2739
2740 :example:
2741
2742 .. code-block:: yaml
2743
2744 policies:
2745 - name: s3-vpc-endpoint-enabled
2746 resource: vpc
2747 filters:
2748 - type: vpc-endpoint
2749 key: ServiceName
2750 value: com.amazonaws.us-east-1.s3
2751 """
2752 RelatedResource = "c7n.resources.vpc.VpcEndpoint"
2753 RelatedIdsExpression = "VpcId"
2754 AnnotationKey = "matched-vpc-endpoint"
2755
2756 schema = type_schema(
2757 'vpc-endpoint',
2758 rinherit=ValueFilter.schema)
2759
2760
2761@Subnet.filter_registry.register("vpc-endpoint")
2762class SubnetEndpointFilter(RelatedResourceByIdFilter):
2763 """Filters subnets based on their vpc-endpoints
2764
2765 :example:
2766
2767 .. code-block:: yaml
2768
2769 policies:
2770 - name: athena-endpoint-enabled
2771 resource: subnet
2772 filters:
2773 - type: vpc-endpoint
2774 key: ServiceName
2775 value: com.amazonaws.us-east-1.athena
2776 """
2777 RelatedResource = "c7n.resources.vpc.VpcEndpoint"
2778 RelatedIdsExpression = "SubnetId"
2779 RelatedResourceByIdExpression = "SubnetIds"
2780 AnnotationKey = "matched-vpc-endpoint"
2781
2782 schema = type_schema(
2783 'vpc-endpoint',
2784 rinherit=ValueFilter.schema)
2785
2786
2787@resources.register('vpc-endpoint-service-configuration')
2788class VPCEndpointServiceConfiguration(query.QueryResourceManager):
2789 """
2790 Resource manager for VPC Endpoint Service Configurations.
2791
2792 :example:
2793
2794 .. code-block:: yaml
2795
2796 policies:
2797 - name: acceptance-not-enabled
2798 resource: aws.vpc-endpoint-service-configuration
2799 filters:
2800 - AcceptanceRequired: false
2801
2802 """
2803 class resource_type(query.TypeInfo):
2804 service = 'ec2'
2805 enum_spec = ('describe_vpc_endpoint_service_configurations',
2806 'ServiceConfigurations', None)
2807 name = id = 'ServiceId' # ServiceName contains DNS
2808 id_prefix = 'vpce-svc-'
2809 filter_name = 'ServiceIds'
2810 filter_type = 'list'
2811 cfn_type = config_type = 'AWS::EC2::VPCEndpointService'
2812 arn_type = 'vpc-endpoint-service'
2813 arn_separator = '/'
2814 default_report_fields = (
2815 'ServiceId',
2816 'ServiceState'
2817 )
2818
2819
2820@resources.register('key-pair')
2821class KeyPair(query.QueryResourceManager):
2822
2823 class resource_type(query.TypeInfo):
2824 service = 'ec2'
2825 arn_type = 'key-pair'
2826 enum_spec = ('describe_key_pairs', 'KeyPairs', None)
2827 name = 'KeyName'
2828 id = 'KeyPairId'
2829 id_prefix = 'key-'
2830 filter_name = 'KeyNames'
2831 filter_type = 'list'
2832
2833
2834@KeyPair.filter_registry.register('unused')
2835class UnusedKeyPairs(Filter):
2836 """Filter for used or unused keys.
2837
2838 The default is unused but can be changed by using the state property.
2839
2840 :example:
2841
2842 .. code-block:: yaml
2843
2844 policies:
2845 - name: unused-key-pairs
2846 resource: aws.key-pair
2847 filters:
2848 - unused
2849 - name: used-key-pairs
2850 resource: aws.key-pair
2851 filters:
2852 - type: unused
2853 state: false
2854 """
2855 schema = type_schema('unused',
2856 state={'type': 'boolean'})
2857
2858 def get_permissions(self):
2859 return list(itertools.chain(*[
2860 self.manager.get_resource_manager(m).get_permissions()
2861 for m in ('asg', 'launch-config', 'ec2')]))
2862
2863 def _pull_asg_keynames(self):
2864 asgs = self.manager.get_resource_manager('asg').resources()
2865 key_names = set()
2866 lcfgs = set(a['LaunchConfigurationName'] for a in asgs if 'LaunchConfigurationName' in a)
2867 lcfg_mgr = self.manager.get_resource_manager('launch-config')
2868
2869 if lcfgs:
2870 key_names.update([
2871 lcfg['KeyName'] for lcfg in lcfg_mgr.resources()
2872 if lcfg['LaunchConfigurationName'] in lcfgs])
2873
2874 tmpl_mgr = self.manager.get_resource_manager('launch-template-version')
2875 for tversion in tmpl_mgr.get_resources(
2876 list(tmpl_mgr.get_asg_templates(asgs).keys())):
2877 key_names.add(tversion['LaunchTemplateData'].get('KeyName'))
2878 return key_names
2879
2880 def _pull_ec2_keynames(self):
2881 ec2_manager = self.manager.get_resource_manager('ec2')
2882 return {i.get('KeyName', None) for i in ec2_manager.resources()}
2883
2884 def process(self, resources, event=None):
2885 keynames = self._pull_ec2_keynames().union(self._pull_asg_keynames())
2886 if self.data.get('state', True):
2887 return [r for r in resources if r['KeyName'] not in keynames]
2888 return [r for r in resources if r['KeyName'] in keynames]
2889
2890
2891@KeyPair.action_registry.register('delete')
2892class DeleteUnusedKeyPairs(BaseAction):
2893 """Delete all ec2 keys that are not in use
2894
2895 This should always be used with the unused filter
2896 and it will prevent you from using without it.
2897
2898 :example:
2899
2900 .. code-block:: yaml
2901
2902 policies:
2903 - name: delete-unused-key-pairs
2904 resource: aws.key-pair
2905 filters:
2906 - unused
2907 actions:
2908 - delete
2909 """
2910 permissions = ('ec2:DeleteKeyPair',)
2911 schema = type_schema('delete')
2912
2913 def validate(self):
2914 if not [f for f in self.manager.iter_filters() if isinstance(f, UnusedKeyPairs)]:
2915 raise PolicyValidationError(
2916 "delete should be used in conjunction with the unused filter on %s" % (
2917 self.manager.data,))
2918 if [True for f in self.manager.iter_filters() if f.data.get('state') is False]:
2919 raise PolicyValidationError(
2920 "You policy has filtered used keys you should use this with unused keys %s" % (
2921 self.manager.data,))
2922 return self
2923
2924 def process(self, unused):
2925 client = local_session(self.manager.session_factory).client('ec2')
2926 for key in unused:
2927 client.delete_key_pair(KeyPairId=key['KeyPairId'])
2928
2929
2930@Vpc.action_registry.register('set-flow-log')
2931@Subnet.action_registry.register('set-flow-log')
2932@NetworkInterface.action_registry.register('set-flow-log')
2933@TransitGateway.action_registry.register('set-flow-log')
2934@TransitGatewayAttachment.action_registry.register('set-flow-log')
2935class SetFlowLogs(BaseAction):
2936 """Set flow logs for a network resource
2937
2938 :example:
2939
2940 .. code-block:: yaml
2941
2942 policies:
2943 - name: vpc-enable-flow-logs
2944 resource: vpc
2945 filters:
2946 - type: flow-logs
2947 enabled: false
2948 actions:
2949 - type: set-flow-log
2950 attrs:
2951 DeliverLogsPermissionArn: arn:iam:role
2952 LogGroupName: /custodian/vpc/flowlogs/
2953
2954 `attrs` are passed through to create_flow_log and are per the api
2955 documentation
2956
2957 https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2/client/create_flow_logs.html
2958 """ # noqa
2959
2960 legacy_schema = {
2961 'DeliverLogsPermissionArn': {'type': 'string'},
2962 'LogGroupName': {'type': 'string'},
2963 'LogDestination': {'type': 'string'},
2964 'LogFormat': {'type': 'string'},
2965 'MaxAggregationInterval': {'type': 'integer'},
2966 'LogDestinationType': {'enum': ['s3', 'cloud-watch-logs']},
2967 'TrafficType': {
2968 'type': 'string',
2969 'enum': ['ACCEPT', 'REJECT', 'ALL']
2970 }
2971 }
2972
2973 schema = type_schema(
2974 'set-flow-log',
2975 state={'type': 'boolean'},
2976 attrs={'type': 'object'},
2977 **legacy_schema
2978 )
2979 shape = 'CreateFlowLogsRequest'
2980 permissions = ('ec2:CreateFlowLogs', 'logs:CreateLogGroup',)
2981
2982 RESOURCE_ALIAS = {
2983 'vpc': 'VPC',
2984 'subnet': 'Subnet',
2985 'eni': 'NetworkInterface',
2986 'transit-gateway': 'TransitGateway',
2987 'transit-attachment': 'TransitGatewayAttachment'
2988 }
2989
2990 def get_deprecations(self):
2991 filter_name = self.data["type"]
2992 return [
2993 DeprecatedField(f"{filter_name}.{k}", f"set {k} under attrs: block")
2994 for k in set(self.legacy_schema).intersection(self.data)
2995 ]
2996
2997 def validate(self):
2998 if set(self.legacy_schema).intersection(self.data) and 'attrs' in self.data:
2999 raise PolicyValidationError(
3000 "set-flow-log: legacy top level keys aren't compatible with `attrs` mapping"
3001 )
3002
3003 self.convert()
3004 attrs = dict(self.data['attrs'])
3005 model = self.manager.get_model()
3006 attrs['ResourceType'] = self.RESOURCE_ALIAS[model.arn_type]
3007 attrs['ResourceIds'] = [model.id_prefix + '123']
3008 return shape_validate(attrs, self.shape, 'ec2')
3009
3010 def convert(self):
3011 data = dict(self.data)
3012 attrs = {}
3013 for k in set(self.legacy_schema).intersection(data):
3014 attrs[k] = data.pop(k)
3015 self.source_data = self.data
3016 self.data['attrs'] = merge_dict(attrs, self.data.get('attrs', {}))
3017
3018 def run_client_op(self, op, params, log_err_codes=()):
3019 try:
3020 results = op(**params)
3021 for r in results['Unsuccessful']:
3022 self.log.exception(
3023 'Exception: %s for %s: %s',
3024 op.__name__, r['ResourceId'], r['Error']['Message'])
3025 except ClientError as e:
3026 if e.response['Error']['Code'] in log_err_codes:
3027 self.log.exception(
3028 'Exception: %s: %s',
3029 op.response['Error']['Message'])
3030 else:
3031 raise
3032
3033 def ensure_log_group(self, logroup):
3034 client = local_session(self.manager.session_factory).client('logs')
3035 try:
3036 client.create_log_group(logGroupName=logroup)
3037 except client.exceptions.ResourceAlreadyExistsException:
3038 pass
3039
3040 def delete_flow_logs(self, client, rids):
3041 flow_logs = [
3042 r for r in self.manager.get_resource_manager('flow-log').resources()
3043 if r['ResourceId'] in rids]
3044 self.run_client_op(
3045 client.delete_flow_logs,
3046 {'FlowLogIds': [f['FlowLogId'] for f in flow_logs]},
3047 ('InvalidParameterValue', 'InvalidFlowLogId.NotFound',)
3048 )
3049
3050 def process(self, resources):
3051 client = local_session(self.manager.session_factory).client('ec2')
3052 enabled = self.data.get('state', True)
3053
3054 if not enabled:
3055 model_id = self.manager.get_model().id
3056 rids = [r[model_id] for r in resources]
3057 return self.delete_flow_logs(client, rids)
3058
3059 model = self.manager.get_model()
3060 params = {'ResourceIds': [r[model.id] for r in resources]}
3061 params['ResourceType'] = self.RESOURCE_ALIAS[model.arn_type]
3062 params.update(self.data['attrs'])
3063 if params.get('LogDestinationType', 'cloud-watch-logs') == 'cloud-watch-logs':
3064 self.ensure_log_group(params['LogGroupName'])
3065 self.run_client_op(
3066 client.create_flow_logs, params, ('FlowLogAlreadyExists',))
3067
3068
3069class PrefixListDescribe(query.DescribeSource):
3070
3071 def get_resources(self, ids, cache=True):
3072 query = {'Filters': [
3073 {'Name': 'prefix-list-id',
3074 'Values': ids}]}
3075 return self.query.filter(self.manager, **query)
3076
3077
3078@resources.register('prefix-list')
3079class PrefixList(query.QueryResourceManager):
3080
3081 class resource_type(query.TypeInfo):
3082 service = 'ec2'
3083 arn_type = 'prefix-list'
3084 enum_spec = ('describe_managed_prefix_lists', 'PrefixLists', None)
3085 config_type = cfn_type = "AWS::EC2::PrefixList"
3086 name = 'PrefixListName'
3087 id = 'PrefixListId'
3088 id_prefix = 'pl-'
3089 universal_taggable = object()
3090
3091 source_mapping = {'describe': PrefixListDescribe}
3092
3093
3094@PrefixList.filter_registry.register('entry')
3095class Entry(Filter):
3096
3097 schema = type_schema(
3098 'entry', rinherit=ValueFilter.schema)
3099 permissions = ('ec2:GetManagedPrefixListEntries',)
3100
3101 annotation_key = 'c7n:prefix-entries'
3102 match_annotation_key = 'c7n:matched-entries'
3103
3104 def process(self, resources, event=None):
3105 client = local_session(self.manager.session_factory).client('ec2')
3106 for r in resources:
3107 if self.annotation_key in r:
3108 continue
3109 r[self.annotation_key] = client.get_managed_prefix_list_entries(
3110 PrefixListId=r['PrefixListId']).get('Entries', ())
3111
3112 vf = ValueFilter(self.data)
3113 vf.annotate = False
3114
3115 results = []
3116 for r in resources:
3117 matched = []
3118 for e in r[self.annotation_key]:
3119 if vf(e):
3120 matched.append(e)
3121 if matched:
3122 results.append(r)
3123 r[self.match_annotation_key] = matched
3124 return results
3125
3126
3127@Subnet.action_registry.register('modify')
3128class SubnetModifyAtrributes(BaseAction):
3129 """Modify subnet attributes.
3130
3131 :example:
3132
3133 .. code-block:: yaml
3134
3135 policies:
3136 - name: turn-on-public-ip-protection
3137 resource: aws.subnet
3138 filters:
3139 - type: value
3140 key: "MapPublicIpOnLaunch.enabled"
3141 value: false
3142 actions:
3143 - type: modify
3144 MapPublicIpOnLaunch: false
3145 """
3146
3147 schema = type_schema(
3148 "modify",
3149 AssignIpv6AddressOnCreation={'type': 'boolean'},
3150 CustomerOwnedIpv4Pool={'type': 'string'},
3151 DisableLniAtDeviceIndex={'type': 'boolean'},
3152 EnableLniAtDeviceIndex={'type': 'integer'},
3153 EnableResourceNameDnsAAAARecordOnLaunch={'type': 'boolean'},
3154 EnableResourceNameDnsARecordOnLaunch={'type': 'boolean'},
3155 EnableDns64={'type': 'boolean'},
3156 MapPublicIpOnLaunch={'type': 'boolean'},
3157 MapCustomerOwnedIpOnLaunch={'type': 'boolean'},
3158 PrivateDnsHostnameTypeOnLaunch={
3159 'type': 'string', 'enum': ['ip-name', 'resource-name']
3160 }
3161 )
3162
3163 permissions = ("ec2:ModifySubnetAttribute",)
3164
3165 def process(self, resources):
3166 client = local_session(self.manager.session_factory).client('ec2')
3167 params = dict(self.data)
3168 params.pop('type')
3169
3170 for k in list(params):
3171 if isinstance(params[k], bool):
3172 params[k] = {'Value': params[k]}
3173
3174 for r in resources:
3175 self.manager.retry(
3176 client.modify_subnet_attribute,
3177 SubnetId=r['SubnetId'], **params)
3178 return resources
3179
3180
3181@resources.register('mirror-session')
3182class TrafficMirrorSession(query.QueryResourceManager):
3183
3184 class resource_type(query.TypeInfo):
3185 service = 'ec2'
3186 enum_spec = ('describe_traffic_mirror_sessions', 'TrafficMirrorSessions', None)
3187 name = id = 'TrafficMirrorSessionId'
3188 config_type = cfn_type = 'AWS::EC2::TrafficMirrorSession'
3189 arn_type = 'traffic-mirror-session'
3190 universal_taggable = object()
3191 id_prefix = 'tms-'
3192
3193
3194@TrafficMirrorSession.action_registry.register('delete')
3195class DeleteTrafficMirrorSession(BaseAction):
3196 """Action to delete traffic mirror session(s)
3197
3198 :example:
3199
3200 .. code-block:: yaml
3201
3202 policies:
3203 - name: traffic-mirror-session-paclength
3204 resource: mirror-session
3205 filters:
3206 - type: value
3207 key: tag:Owner
3208 value: xyz
3209 actions:
3210 - delete
3211 """
3212
3213 schema = type_schema('delete')
3214 permissions = ('ec2:DeleteTrafficMirrorSession',)
3215
3216 def process(self, resources):
3217 client = local_session(self.manager.session_factory).client('ec2')
3218 for r in resources:
3219 client.delete_traffic_mirror_session(TrafficMirrorSessionId=r['TrafficMirrorSessionId'])
3220
3221
3222@resources.register('mirror-target')
3223class TrafficMirrorTarget(query.QueryResourceManager):
3224
3225 class resource_type(query.TypeInfo):
3226 service = 'ec2'
3227 enum_spec = ('describe_traffic_mirror_targets', 'TrafficMirrorTargets', None)
3228 name = id = 'TrafficMirrorTargetId'
3229 config_type = cfn_type = 'AWS::EC2::TrafficMirrorTarget'
3230 arn_type = 'traffic-mirror-target'
3231 universal_taggable = object()
3232 id_prefix = 'tmt-'
3233
3234
3235@RouteTable.filter_registry.register('cross-az-nat-gateway-route')
3236class CrossAZRouteTable(Filter):
3237 """Filter route-tables to find those with routes which send traffic
3238 from a subnet in an az to a nat gateway in a different az.
3239
3240 This filter is useful for cost optimization, resiliency, and
3241 performance use-cases, where we don't want network traffic to
3242 cross from one availability zone (AZ) to another AZ.
3243
3244 :Example:
3245
3246 .. code-block:: yaml
3247
3248 policies:
3249 - name: cross-az-nat-gateway-traffic
3250 resource: aws.route-table
3251 filters:
3252 - type: cross-az-nat-gateway-route
3253 actions:
3254 - notify
3255
3256 """
3257 schema = type_schema('cross-az-nat-gateway-route')
3258 permissions = ("ec2:DescribeRouteTables", "ec2:DescribeNatGateways", "ec2:DescribeSubnets")
3259
3260 table_annotation = "c7n:route-table"
3261 mismatch_annotation = "c7n:nat-az-mismatch"
3262
3263 def resolve_subnets(self, resource, subnets):
3264 return {s['SubnetId'] for s in subnets
3265 if s[self.table_annotation] == resource['RouteTableId']}
3266
3267 def annotate_subnets_table(self, tables: list, subnets: dict):
3268 # annotate route table associations onto their respective subnets
3269 main_tables = []
3270 # annotate explicit associations
3271 for t in tables:
3272 for association in t['Associations']:
3273 if association.get('SubnetId'):
3274 subnets[association['SubnetId']][
3275 self.table_annotation] = t['RouteTableId']
3276 if association.get('Main'):
3277 main_tables.append(t)
3278 # annotate main tables
3279 for s in subnets.values():
3280 if self.table_annotation in s:
3281 continue
3282 for t in main_tables:
3283 if t['VpcId'] == s['VpcId']:
3284 s[self.table_annotation] = t['RouteTableId']
3285
3286 def process_route_table(self, subnets, nat_subnets, resource):
3287 matched = {}
3288 found = False
3289 associated_subnets = self.resolve_subnets(resource, subnets.values())
3290 for route in resource['Routes']:
3291 if not route.get("NatGatewayId") or route.get("State") != "active":
3292 continue
3293 nat_az = subnets[nat_subnets[route['NatGatewayId']]]['AvailabilityZone']
3294 mismatch_subnets = {
3295 s: subnets[s]['AvailabilityZone'] for s in associated_subnets
3296 if subnets[s]['AvailabilityZone'] != nat_az}
3297 if not mismatch_subnets:
3298 continue
3299 found = True
3300 matched.setdefault(route['NatGatewayId'], {})['NatGatewayAz'] = nat_az
3301 matched[route['NatGatewayId']].setdefault('Subnets', {}).update(mismatch_subnets)
3302 if not found:
3303 return
3304 resource[self.mismatch_annotation] = matched
3305 return resource
3306
3307 def process(self, resources, event=None):
3308 subnets = {
3309 s['SubnetId']: s for s in
3310 self.manager.get_resource_manager('aws.subnet').resources()
3311 }
3312 nat_subnets = {
3313 nat_gateway['NatGatewayId']: nat_gateway["SubnetId"]
3314 for nat_gateway in self.manager.get_resource_manager('nat-gateway').resources()}
3315
3316 results = []
3317 self.annotate_subnets_table(resources, subnets)
3318 for resource in resources:
3319 if self.process_route_table(subnets, nat_subnets, resource):
3320 results.append(resource)
3321
3322 return results
3323
3324
3325@NetworkAddress.filter_registry.register('used-by')
3326class UsedByNetworkAddress(Filter):
3327 """Filter Elastic IPs to find the resource type that the network
3328 interface that the Elastic IP is associated with is attached to.
3329
3330 This filter is useful for limiting the types of resources to
3331 enable AWS Shield Advanced protection.
3332
3333 :Example:
3334
3335 .. code-block:: yaml
3336
3337 policies:
3338 - name: eip-shield-advanced-enable
3339 resource: aws.elastic-ip
3340 filters:
3341 - type: used-by
3342 resource-type: elb-net
3343 - type: shield-enabled
3344 state: false
3345 actions:
3346 - type: set-shield
3347 state: true
3348 """
3349 schema = type_schema(
3350 'used-by', required=['resource-type'], **{
3351 'resource-type': {'type': 'string'}}
3352 )
3353 permissions = ("ec2:DescribeNetworkInterfaces",)
3354
3355 def process(self, resources, event=None):
3356 eni_ids = []
3357 for r in resources:
3358 if r.get('NetworkInterfaceId'):
3359 eni_ids.append(r['NetworkInterfaceId'])
3360 enis = self.manager.get_resource_manager('eni').get_resources(eni_ids)
3361 results = []
3362 for r in resources:
3363 for eni in enis:
3364 if r.get('NetworkInterfaceId') == eni['NetworkInterfaceId']:
3365 rtype = get_eni_resource_type(eni)
3366 if rtype == self.data.get('resource-type'):
3367 results.append(r)
3368 return results