1# Copyright The Cloud Custodian Authors.
2# SPDX-License-Identifier: Apache-2.0
3from botocore.exceptions import ClientError
4from botocore.paginate import Paginator
5
6from c7n.actions import BaseAction
7from c7n.filters import Filter
8from c7n.manager import resources
9from c7n.query import (QueryResourceManager, RetryPageIterator, TypeInfo,
10 DescribeSource, ConfigSource)
11from c7n.tags import RemoveTag, Tag, TagDelayedAction, TagActionFilter
12from c7n.utils import local_session, type_schema, get_retry
13
14
15class DescribeShieldProtection(DescribeSource):
16 def augment(self, resources):
17 client = local_session(self.manager.session_factory).client('shield')
18
19 def _augment(r):
20 tags = self.manager.retry(client.list_tags_for_resource,
21 ResourceARN=r['ProtectionArn'])['Tags']
22 r['Tags'] = tags
23 return r
24 resources = super().augment(resources)
25 return list(map(_augment, resources))
26
27
28@resources.register('shield-protection')
29class ShieldProtection(QueryResourceManager):
30 class resource_type(TypeInfo):
31 service = 'shield'
32 enum_spec = ('list_protections', 'Protections', None)
33 id = 'Id'
34 name = 'Name'
35 arn = 'ProtectionArn'
36 config_type = 'AWS::Shield::Protection'
37 global_resource = True
38
39 source_mapping = {
40 'describe': DescribeShieldProtection,
41 'config': ConfigSource
42 }
43
44
45@resources.register('shield-attack')
46class ShieldAttack(QueryResourceManager):
47
48 class resource_type(TypeInfo):
49 service = 'shield'
50 enum_spec = ('list_attacks', 'Attacks', None)
51 detail_spec = (
52 'describe_attack', 'AttackId', 'AttackId', 'Attack')
53 name = id = 'AttackId'
54 date = 'StartTime'
55 filter_name = 'ResourceArns'
56 filter_type = 'list'
57 arn = False
58 global_resource = True
59
60
61def get_protections_paginator(client):
62 return Paginator(
63 client.list_protections,
64 {'input_token': 'NextToken', 'output_token': 'NextToken', 'result_key': 'Protections'},
65 client.meta.service_model.operation_model('ListProtections'))
66
67
68def get_type_protections(client, arn_type):
69 pager = get_protections_paginator(client)
70 pager.PAGE_ITERATOR_CLS = RetryPageIterator
71 try:
72 protections = pager.paginate().build_full_result().get('Protections', [])
73 except client.exceptions.ResourceNotFoundException:
74 # shield is not enabled in the account, so all resources are not protected
75 return []
76 return [p for p in protections if arn_type in p['ResourceArn']]
77
78
79ShieldRetry = get_retry(('ThrottlingException',))
80
81
82class ProtectedResource:
83 """Base class with helper methods for dealing with
84 ARNs of resources protected by Shield
85 """
86
87 def get_arns(self, resources):
88 return self.manager.get_arns(resources)
89
90 @property
91 def arn_type(self):
92 return self.manager.get_model().arn_type
93
94
95class IsShieldProtected(Filter, ProtectedResource):
96
97 permissions = ('shield:ListProtections',)
98 schema = type_schema('shield-enabled', state={'type': 'boolean'})
99
100 def process(self, resources, event=None):
101 client = local_session(self.manager.session_factory).client(
102 'shield', region_name='us-east-1')
103
104 protections = get_type_protections(client, self.arn_type)
105 protected_resources = {p['ResourceArn'] for p in protections}
106
107 state = self.data.get('state', False)
108 results = []
109
110 for arn, r in zip(self.get_arns(resources), resources):
111 r['c7n:ShieldProtected'] = shielded = arn in protected_resources
112 if shielded and state:
113 results.append(r)
114 elif not shielded and not state:
115 results.append(r)
116
117 return results
118
119
120class SetShieldProtection(BaseAction, ProtectedResource):
121 """Enable shield protection on applicable resource.
122
123 setting `sync` parameter will also clear out stale shield protections
124 for resources that no longer exist.
125 """
126
127 permissions = ('shield:CreateProtection', 'shield:ListProtections',)
128 schema = type_schema(
129 'set-shield',
130 state={'type': 'boolean'}, sync={'type': 'boolean'})
131
132 def process(self, resources):
133 client = local_session(self.manager.session_factory).client(
134 'shield', region_name='us-east-1')
135 model = self.manager.get_model()
136 protections = get_type_protections(client, self.arn_type)
137 protected_resources = {p['ResourceArn']: p for p in protections}
138 state = self.data.get('state', True)
139
140 if self.data.get('sync', False):
141 self.clear_stale(client, protections)
142
143 for arn, r in zip(self.get_arns(resources), resources):
144 if state and arn in protected_resources:
145 continue
146 if state is False and arn in protected_resources:
147 ShieldRetry(
148 client.delete_protection,
149 ProtectionId=protected_resources[arn]['Id'])
150 continue
151 try:
152 ShieldRetry(
153 client.create_protection,
154 Name=r[model.name], ResourceArn=arn)
155 except ClientError as e:
156 if e.response['Error']['Code'] == 'ResourceAlreadyExistsException':
157 continue
158 raise
159
160 def clear_stale(self, client, protections):
161 # Get all resources unfiltered
162 resources = self.manager.get_resource_manager(
163 self.manager.type).resources()
164 resource_arns = set(self.get_arns(resources))
165
166 pmap = {}
167 # Only process stale resources in region for non global resources.
168 global_resource = getattr(self.manager.resource_type, 'global_resource', False)
169 for p in protections:
170 if not global_resource and self.manager.region not in p['ResourceArn']:
171 continue
172 pmap[p['ResourceArn']] = p
173
174 # Find any protections for resources that don't exist
175 stale = set(pmap).difference(resource_arns)
176 self.log.info("clearing %d stale protections", len(stale))
177 for s in stale:
178 ShieldRetry(
179 client.delete_protection, ProtectionId=pmap[s]['Id'])
180
181
182class ProtectedEIP:
183 """Contains helper methods for dealing with Elastic IP within Shield API calls.
184 The Elastic IP resource type as described in IAM is "elastic-ip":
185 https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonec2.html#amazonec2-elastic-ip
186
187 But Shield requires the resource type to be "eip-allocation":
188 https://docs.aws.amazon.com/waf/latest/DDOSAPIReference/API_CreateProtection.html
189 """
190
191 def get_arns(self, resources):
192 arns = [
193 arn.replace(':elastic-ip', ':eip-allocation')
194 if ':elastic-ip' in arn else arn
195 for arn in
196 self.manager.get_arns(resources)
197 ]
198 return arns
199
200 @property
201 def arn_type(self):
202 return 'eip-allocation'
203
204
205class IsEIPShieldProtected(ProtectedEIP, IsShieldProtected):
206 pass
207
208
209class SetEIPShieldProtection(ProtectedEIP, SetShieldProtection):
210 pass
211
212
213@ShieldProtection.action_registry.register('tag')
214class TagResource(Tag):
215 """Action to tag a Shield resources
216 """
217 permissions = ('shield:TagResource',)
218
219 def process_resource_set(self, client, resource_set, tags):
220 mid = self.manager.resource_type.arn
221 for r in resource_set:
222 try:
223 client.tag_resource(ResourceARN=r[mid], Tags=tags)
224 except client.exceptions.ResourceNotFoundException:
225 continue
226
227
228@ShieldProtection.action_registry.register('remove-tag')
229class RemoveTag(RemoveTag):
230 """Action to remove tags from a Shield resource
231 """
232 permissions = ('shield:UntagResource',)
233
234 def process_resource_set(self, client, resource_set, tag_keys):
235 mid = self.manager.resource_type.arn
236 for r in resource_set:
237 try:
238 client.untag_resource(ResourceARN=r[mid], TagKeys=tag_keys)
239 except client.exceptions.ResourceNotFoundException:
240 continue
241
242
243@ShieldProtection.filter_registry.register('marked-for-op', TagActionFilter)
244@ShieldProtection.action_registry.register('mark-for-op')
245class MarkShieldProtectionForOp(TagDelayedAction):
246 """Mark Shield Protection for deferred action
247
248 :example:
249
250 .. code-block:: yaml
251
252 policies:
253 - name: shield-protection-invalid-tag-mark
254 resource: shield-protection
255 filters:
256 - "tag:InvalidTag": present
257 actions:
258 - type: mark-for-op
259 op: delete
260 days: 1
261 """