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