Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/c7n/resources/ecr.py: 38%
252 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 json
5from c7n.actions import RemovePolicyBase, Action, ModifyPolicyBase
6from c7n.exceptions import PolicyValidationError
7from c7n.filters import CrossAccountAccessFilter, Filter, ValueFilter, MetricsFilter
8from c7n.manager import resources
9from c7n.query import (
10 ConfigSource, DescribeSource, QueryResourceManager, TypeInfo,
11 ChildResourceManager, ChildDescribeSource, ChildResourceQuery, sources)
12from c7n import tags
13from c7n.utils import local_session, type_schema
16class DescribeECR(DescribeSource):
18 def augment(self, resources):
19 client = local_session(self.manager.session_factory).client('ecr')
20 results = []
21 for r in resources:
22 try:
23 r['Tags'] = client.list_tags_for_resource(
24 resourceArn=r['repositoryArn']).get('tags')
25 results.append(r)
26 except client.exceptions.RepositoryNotFoundException:
27 continue
28 return results
31@resources.register('ecr')
32class ECR(QueryResourceManager):
34 class resource_type(TypeInfo):
35 service = 'ecr'
36 enum_spec = ('describe_repositories', 'repositories', None)
37 id = name = "repositoryName"
38 arn = "repositoryArn"
39 arn_type = 'repository'
40 filter_name = 'repositoryNames'
41 filter_type = 'list'
42 config_type = cfn_type = 'AWS::ECR::Repository'
43 dimension = 'RepositoryName'
45 source_mapping = {
46 'describe': DescribeECR,
47 'config': ConfigSource
48 }
51@ECR.filter_registry.register('metrics')
52class ECRMetricsFilter(MetricsFilter):
53 def get_dimensions(self, resource):
54 return [{"Name": "RepositoryName", "Value": resource['repositoryName']}]
57class ECRImageQuery(ChildResourceQuery):
59 def get(self, resource_manager, identities):
60 m = self.resolve(resource_manager.resource_type)
61 params = {}
62 resources = self.filter(resource_manager, **params)
63 resources = [r for r in resources if "{}/{}".format(r[0], r[1][m.id]) in identities]
65 return resources
68@sources.register('describe-ecr-image')
69class RepositoryImageDescribeSource(ChildDescribeSource):
71 resource_query_factory = ECRImageQuery
73 def get_query(self):
74 query = super(RepositoryImageDescribeSource, self).get_query()
75 query.capture_parent_id = True
76 return query
78 def augment(self, resources):
79 results = []
80 client = local_session(self.manager.session_factory).client('ecr')
81 for repositoryName, image in resources:
82 repoArn = client.describe_repositories(
83 repositoryNames=[repositoryName])['repositories'][0]['repositoryArn']
84 imageArn = "{}/{}".format(repoArn, image["imageDigest"])
85 image["imageArn"] = imageArn
86 results.append(image)
87 return results
90@resources.register('ecr-image')
91class RepositoryImage(ChildResourceManager):
93 class resource_type(TypeInfo):
94 service = 'ecr'
95 parent_spec = ('ecr', 'repositoryName', None)
96 enum_spec = ('describe_images', 'imageDetails', None)
97 id = 'imageDigest'
98 name = 'repositoryName'
99 arn = "imageArn"
100 arn_type = 'repository'
102 source_mapping = {
103 'describe-child': RepositoryImageDescribeSource,
104 'describe': RepositoryImageDescribeSource,
105 }
108ECR_POLICY_SCHEMA = {
109 'type': 'object',
110 'properties': {
111 'Sid': {'type': 'string'},
112 'Effect': {'type': 'string', 'enum': ['Allow', 'Deny']},
113 'Principal': {'anyOf': [
114 {'type': 'string'},
115 {'type': 'object'}, {'type': 'array'}]},
116 'NotPrincipal': {'anyOf': [{'type': 'object'}, {'type': 'array'}]},
117 'Action': {'anyOf': [{'type': 'string', 'pattern': '^ecr:([a-zA-Z]*|[*])$'},
118 {'type': 'array', 'items': {'type': 'string', 'pattern': '^ecr:([a-zA-Z]*|[*])$'}}]},
119 'NotAction': {'anyOf': [{'type': 'string', 'pattern': '^ecr:([a-zA-Z]*|[*])$'},
120 {'type': 'array', 'items': {'type': 'string', 'pattern': '^ecr:([a-zA-Z]*|[*])$'}}]},
121 'Resource': {'anyOf': [{'type': 'string'}, {'type': 'array'}]},
122 'NotResource': {'anyOf': [{'type': 'string'}, {'type': 'array'}]},
123 'Condition': {'type': 'object'}
124 },
125 'required': ['Sid', 'Effect'],
126 'oneOf': [
127 {'required': ['Principal', 'Action']},
128 {'required': ['NotPrincipal', 'Action']},
129 {'required': ['Principal', 'NotAction']},
130 {'required': ['NotPrincipal', 'NotAction']}
131 ]
132}
135@RepositoryImage.action_registry.register('modify-ecr-policy')
136@ECR.action_registry.register('modify-ecr-policy')
137class ModifyPolicyStatement(ModifyPolicyBase):
138 """Action to modify ECR policy statements.
140 :example:
142 .. code-block:: yaml
144 policies:
145 - name: ecr-image-prevent-pull
146 resource: ecr-image
147 filters:
148 - type: finding
149 actions:
150 - type: modify-ecr-policy
151 add-statements: [{
152 "Sid": "ReplaceWithMe",
153 "Effect": "Deny",
154 "Principal": "*",
155 "Action": ["ecr:BatchGetImage"]
156 }]
157 remove-statements: "*"
158 """
159 permissions = ('ecr:GetRepositoryPolicy', 'ecr:SetRepositoryPolicy')
160 schema = type_schema(
161 'modify-ecr-policy', schema_alias=False,
162 **{
163 'add-statements': {
164 'type': 'array',
165 'items': ECR_POLICY_SCHEMA,
166 },
167 'remove-statements': {
168 'type': ['array', 'string'],
169 'oneOf': [
170 {'enum': ['matched', '*']},
171 {'type': 'array', 'items': {'type': 'string'}}
172 ],
173 }
174 })
176 def process(self, resources):
177 results = []
178 client = local_session(self.manager.session_factory).client('ecr')
179 for r in resources:
180 try:
181 policy = json.loads(
182 client.get_repository_policy(
183 repositoryName=r["repositoryName"])["policyText"])
184 except client.exceptions.RepositoryPolicyNotFoundException:
185 policy = {}
186 policy_statements = policy.setdefault('Statement', [])
187 new_policy, removed = self.remove_statements(
188 policy_statements, r, CrossAccountAccessFilter.annotation_key)
189 if new_policy is None:
190 new_policy = policy_statements
191 new_policy, added = self.add_statements(new_policy)
193 if not removed and not added:
194 continue
195 elif not new_policy:
196 client.delete_repository_policy(
197 repositoryName=r['repositoryName'])
198 else:
199 cleaned = []
200 for statement in new_policy:
201 if "Resource" in statement:
202 del statement["Resource"]
203 cleaned.append(statement)
204 else:
205 cleaned.append(statement)
206 policy['Statement'] = cleaned
207 client.set_repository_policy(
208 repositoryName=r['repositoryName'],
209 policyText=json.dumps(policy))
210 results += {
211 'Name': r['repositoryName'],
212 'State': 'PolicyModified',
213 'Statements': new_policy
214 }
216 return results
219@ECR.action_registry.register('tag')
220class ECRTag(tags.Tag):
222 permissions = ('ecr:TagResource',)
224 def process_resource_set(self, client, resources, tags):
225 for r in resources:
226 try:
227 client.tag_resource(resourceArn=r['repositoryArn'], tags=tags)
228 except client.exceptions.RepositoryNotFoundException:
229 pass
232@ECR.action_registry.register('set-scanning')
233class ECRSetScanning(Action):
235 permissions = ('ecr:PutImageScanningConfiguration',)
236 schema = type_schema(
237 'set-scanning',
238 state={'type': 'boolean', 'default': True})
240 def process(self, resources):
241 client = local_session(self.manager.session_factory).client('ecr')
242 s = self.data.get('state', True)
243 for r in resources:
244 try:
245 client.put_image_scanning_configuration(
246 registryId=r['registryId'],
247 repositoryName=r['repositoryName'],
248 imageScanningConfiguration={
249 'scanOnPush': s})
250 except client.exceptions.RepositoryNotFoundException:
251 continue
254@ECR.action_registry.register('set-immutability')
255class ECRSetImmutability(Action):
257 permissions = ('ecr:PutImageTagMutability',)
258 schema = type_schema(
259 'set-immutability',
260 state={'type': 'boolean', 'default': True})
262 def process(self, resources):
263 client = local_session(self.manager.session_factory).client('ecr')
264 s = 'IMMUTABLE' if self.data.get('state', True) else 'MUTABLE'
265 for r in resources:
266 try:
267 client.put_image_tag_mutability(
268 registryId=r['registryId'],
269 repositoryName=r['repositoryName'],
270 imageTagMutability=s)
271 except client.exceptions.RepositoryNotFoundException:
272 continue
275@ECR.action_registry.register('remove-tag')
276class ECRRemoveTags(tags.RemoveTag):
278 permissions = ('ecr:UntagResource',)
280 def process_resource_set(self, client, resources, tags):
281 for r in resources:
282 try:
283 client.untag_resource(resourceArn=r['repositoryArn'], tagKeys=tags)
284 except client.exceptions.RepositoryNotFoundException:
285 pass
288ECR.filter_registry.register('marked-for-op', tags.TagActionFilter)
289ECR.action_registry.register('mark-for-op', tags.TagDelayedAction)
292@ECR.filter_registry.register('cross-account')
293class ECRCrossAccountAccessFilter(CrossAccountAccessFilter):
294 """Filters all EC2 Container Registries (ECR) with cross-account access
296 :example:
298 .. code-block:: yaml
300 policies:
301 - name: ecr-cross-account
302 resource: ecr
303 filters:
304 - type: cross-account
305 whitelist_from:
306 expr: "accounts.*.accountNumber"
307 url: accounts_url
308 """
309 permissions = ('ecr:GetRepositoryPolicy',)
311 def process(self, resources, event=None):
313 client = local_session(self.manager.session_factory).client('ecr')
315 def _augment(r):
316 try:
317 r['Policy'] = client.get_repository_policy(
318 repositoryName=r['repositoryName'])['policyText']
319 except client.exceptions.RepositoryPolicyNotFoundException:
320 return None
321 return r
323 self.log.debug("fetching policy for %d repos" % len(resources))
324 with self.executor_factory(max_workers=2) as w:
325 resources = list(filter(None, w.map(_augment, resources)))
327 return super(ECRCrossAccountAccessFilter, self).process(resources, event)
330LIFECYCLE_RULE_SCHEMA = {
331 'type': 'object',
332 'additionalProperties': False,
333 'required': ['rulePriority', 'action', 'selection'],
334 'properties': {
335 'rulePriority': {'type': 'integer'},
336 'description': {'type': 'string'},
337 'action': {
338 'type': 'object',
339 'required': ['type'],
340 'additionalProperties': False,
341 'properties': {'type': {'enum': ['expire']}}},
342 'selection': {
343 'type': 'object',
344 'addtionalProperties': False,
345 'required': ['countType', 'countNumber', 'tagStatus'],
346 'properties': {
347 'tagStatus': {'enum': ['tagged', 'untagged', 'any']},
348 'tagPrefixList': {'type': 'array', 'items': {'type': 'string'}},
349 'countNumber': {'type': 'integer'},
350 'countUnit': {'enum': ['hours', 'days']},
351 'countType': {
352 'enum': ['imageCountMoreThan', 'sinceImagePushed']},
353 }
354 }
355 }
356}
359def lifecycle_rule_validate(policy, rule):
360 # This is a non exhaustive list of lifecycle validation rules
361 # see this for a more comprehensive list
362 #
363 # https://docs.aws.amazon.com/AmazonECR/latest/userguide/LifecyclePolicies.html#lp_evaluation_rules
365 if (rule['selection']['tagStatus'] == 'tagged' and
366 'tagPrefixList' not in rule['selection']):
367 raise PolicyValidationError(
368 ("{} has invalid lifecycle rule {} tagPrefixList "
369 "required for tagStatus: tagged").format(
370 policy.name, rule))
371 if (rule['selection']['countType'] == 'sinceImagePushed' and
372 'countUnit' not in rule['selection']):
373 raise PolicyValidationError(
374 ("{} has invalid lifecycle rule {} countUnit "
375 "required for countType: sinceImagePushed").format(
376 policy.name, rule))
377 if (rule['selection']['countType'] == 'imageCountMoreThan' and
378 'countUnit' in rule['selection']):
379 raise PolicyValidationError(
380 ("{} has invalid lifecycle rule {} countUnit "
381 "invalid for countType: imageCountMoreThan").format(
382 policy.name, rule))
385@ECR.filter_registry.register('lifecycle-rule')
386class LifecycleRule(Filter):
387 """Lifecycle rule filtering
389 :Example:
391 .. code-block:: yaml
393 policies:
394 - name: ecr-life
395 resource: aws.ecr
396 filters:
397 - type: lifecycle-rule
398 state: False
399 match:
400 - selection.tagStatus: untagged
401 - action.type: expire
402 - type: value
403 key: selection.countNumber
404 value: 30
405 op: less-than
406 """
407 permissions = ('ecr:GetLifecyclePolicy',)
408 schema = type_schema(
409 'lifecycle-rule',
410 state={'type': 'boolean'},
411 match={'type': 'array', 'items': {
412 'oneOf': [
413 {'$ref': '#/definitions/filters/value'},
414 {'type': 'object', 'minProperties': 1, 'maxProperties': 1},
415 ]}})
416 policy_annotation = 'c7n:lifecycle-policy'
418 def process(self, resources, event=None):
419 client = local_session(self.manager.session_factory).client('ecr')
420 for r in resources:
421 if self.policy_annotation in r:
422 continue
423 try:
424 r[self.policy_annotation] = json.loads(
425 client.get_lifecycle_policy(
426 repositoryName=r['repositoryName']).get(
427 'lifecyclePolicyText', ''))
428 except client.exceptions.LifecyclePolicyNotFoundException:
429 r[self.policy_annotation] = {}
431 state = self.data.get('state', False)
432 matchers = []
433 for matcher in self.data.get('match', []):
434 vf = ValueFilter(matcher)
435 vf.annotate = False
436 matchers.append(vf)
438 results = []
439 for r in resources:
440 found = False
441 for rule in r[self.policy_annotation].get('rules', []):
442 found = True
443 for m in matchers:
444 if not m(rule):
445 found = False
446 if found and state:
447 results.append(r)
448 if not found and not state:
449 results.append(r)
450 return results
453@ECR.action_registry.register('set-lifecycle')
454class SetLifecycle(Action):
455 """Set the lifecycle policy for ECR repositories.
458 Note at the moment this is limited to set/delete/replacement of
459 lifecycle policies, not merge.
460 """
461 permissions = ('ecr:PutLifecyclePolicy', 'ecr:DeleteLifecyclePolicy')
463 schema = type_schema(
464 'set-lifecycle',
465 state={'type': 'boolean'},
466 rules={
467 'type': 'array',
468 'items': LIFECYCLE_RULE_SCHEMA})
470 def validate(self):
471 if self.data.get('state') is False and 'rules' in self.data:
472 raise PolicyValidationError(
473 "set-lifecycle can't use statements and state: false")
474 elif self.data.get('state', True) and not self.data.get('rules'):
475 raise PolicyValidationError(
476 "set-lifecycle requires rules with state: true")
477 for r in self.data.get('rules', []):
478 lifecycle_rule_validate(self.manager.ctx.policy, r)
479 return self
481 def process(self, resources):
482 client = local_session(self.manager.session_factory).client('ecr')
483 state = self.data.get('state', True)
484 for r in resources:
485 if state is False:
486 try:
487 client.delete_lifecycle_policy(
488 registryId=r['registryId'],
489 repositoryName=r['repositoryName'])
490 continue
491 except client.exceptions.LifecyclePolicyNotFoundException:
492 pass
493 client.put_lifecycle_policy(
494 registryId=r['registryId'],
495 repositoryName=r['repositoryName'],
496 lifecyclePolicyText=json.dumps({'rules': self.data['rules']}))
499@ECR.action_registry.register('remove-statements')
500class RemovePolicyStatement(RemovePolicyBase):
501 """Action to remove policy statements from ECR
503 :example:
505 .. code-block:: yaml
507 policies:
508 - name: ecr-remove-cross-accounts
509 resource: ecr
510 filters:
511 - type: cross-account
512 actions:
513 - type: remove-statements
514 statement_ids: matched
515 """
517 permissions = ("ecr:SetRepositoryPolicy", "ecr:GetRepositoryPolicy")
519 def process(self, resources):
520 results = []
521 client = local_session(self.manager.session_factory).client('ecr')
522 for r in resources:
523 try:
524 results += filter(None, [self.process_resource(client, r)])
525 except Exception:
526 self.log.exception(
527 "Error processing ecr registry:%s", r['repositoryArn'])
528 return results
530 def process_resource(self, client, resource):
531 if 'Policy' not in resource:
532 try:
533 resource['Policy'] = client.get_repository_policy(
534 repositoryName=resource['repositoryName'])['policyText']
535 except client.exceptions.RepositoryPolicyNotFoundException:
536 return
538 p = json.loads(resource['Policy'])
539 statements, found = self.process_policy(
540 p, resource, CrossAccountAccessFilter.annotation_key)
542 if not found:
543 return
545 if not statements:
546 client.delete_repository_policy(
547 repositoryName=resource['repositoryName'])
548 else:
549 client.set_repository_policy(
550 repositoryName=resource['repositoryName'],
551 policyText=json.dumps(p))
552 return {'Name': resource['repositoryName'],
553 'State': 'PolicyRemoved',
554 'Statements': found}