Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/c7n/resources/shield.py: 46%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

148 statements  

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 if e.response['Error']['Code'] == 'InvalidParameterException': 

159 # CloudFront distributions with pricing plans cannot have Shield Advanced 

160 # enabled. Skip these resources gracefully. 

161 # https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/flat-rate-pricing-plan.html 

162 if 'CloudFront Pricing Plan' in str(e): 

163 self.log.warning( 

164 "Skipping Shield protection for %s: distribution has a " 

165 "CloudFront pricing plan subscription which does not support " 

166 "Shield Advanced", arn) 

167 continue 

168 raise 

169 

170 def clear_stale(self, client, protections): 

171 # Get all resources unfiltered 

172 resources = self.manager.get_resource_manager( 

173 self.manager.type).resources() 

174 resource_arns = set(self.get_arns(resources)) 

175 

176 pmap = {} 

177 # Only process stale resources in region for non global resources. 

178 global_resource = getattr(self.manager.resource_type, 'global_resource', False) 

179 for p in protections: 

180 if not global_resource and self.manager.region not in p['ResourceArn']: 

181 continue 

182 pmap[p['ResourceArn']] = p 

183 

184 # Find any protections for resources that don't exist 

185 stale = set(pmap).difference(resource_arns) 

186 self.log.info("clearing %d stale protections", len(stale)) 

187 for s in stale: 

188 ShieldRetry( 

189 client.delete_protection, ProtectionId=pmap[s]['Id']) 

190 

191 

192class ProtectedEIP: 

193 """Contains helper methods for dealing with Elastic IP within Shield API calls. 

194 The Elastic IP resource type as described in IAM is "elastic-ip": 

195 https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonec2.html#amazonec2-elastic-ip 

196 

197 But Shield requires the resource type to be "eip-allocation": 

198 https://docs.aws.amazon.com/waf/latest/DDOSAPIReference/API_CreateProtection.html 

199 """ 

200 

201 def get_arns(self, resources): 

202 arns = [ 

203 arn.replace(':elastic-ip', ':eip-allocation') 

204 if ':elastic-ip' in arn else arn 

205 for arn in 

206 self.manager.get_arns(resources) 

207 ] 

208 return arns 

209 

210 @property 

211 def arn_type(self): 

212 return 'eip-allocation' 

213 

214 

215class IsEIPShieldProtected(ProtectedEIP, IsShieldProtected): 

216 pass 

217 

218 

219class SetEIPShieldProtection(ProtectedEIP, SetShieldProtection): 

220 pass 

221 

222 

223@ShieldProtection.action_registry.register('tag') 

224class TagResource(Tag): 

225 """Action to tag a Shield resources 

226 """ 

227 permissions = ('shield:TagResource',) 

228 

229 def process_resource_set(self, client, resource_set, tags): 

230 mid = self.manager.resource_type.arn 

231 for r in resource_set: 

232 try: 

233 client.tag_resource(ResourceARN=r[mid], Tags=tags) 

234 except client.exceptions.ResourceNotFoundException: 

235 continue 

236 

237 

238@ShieldProtection.action_registry.register('remove-tag') 

239class RemoveTag(RemoveTag): 

240 """Action to remove tags from a Shield resource 

241 """ 

242 permissions = ('shield:UntagResource',) 

243 

244 def process_resource_set(self, client, resource_set, tag_keys): 

245 mid = self.manager.resource_type.arn 

246 for r in resource_set: 

247 try: 

248 client.untag_resource(ResourceARN=r[mid], TagKeys=tag_keys) 

249 except client.exceptions.ResourceNotFoundException: 

250 continue 

251 

252 

253@ShieldProtection.filter_registry.register('marked-for-op', TagActionFilter) 

254@ShieldProtection.action_registry.register('mark-for-op') 

255class MarkShieldProtectionForOp(TagDelayedAction): 

256 """Mark Shield Protection for deferred action 

257 

258 :example: 

259 

260 .. code-block:: yaml 

261 

262 policies: 

263 - name: shield-protection-invalid-tag-mark 

264 resource: shield-protection 

265 filters: 

266 - "tag:InvalidTag": present 

267 actions: 

268 - type: mark-for-op 

269 op: delete 

270 days: 1 

271 """