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

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

270 statements  

1# Copyright The Cloud Custodian Authors. 

2# SPDX-License-Identifier: Apache-2.0 

3import json 

4 

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 generate_arn, local_session, type_schema 

14 

15 

16class ConfigECR(ConfigSource): 

17 

18 def load_resource(self, item): 

19 resource = super().load_resource(item) 

20 for configk, servicek in { 

21 'RepositoryName': 'repositoryName', 

22 'Arn': 'repositoryArn', 

23 'RepositoryUri': 'repositoryUri', 

24 'RepositoryPolicyText': 'Policy'}.items(): 

25 resource[servicek] = resource.pop(configk, None) 

26 return resource 

27 

28 

29class DescribeECR(DescribeSource): 

30 

31 def augment(self, resources): 

32 client = local_session(self.manager.session_factory).client('ecr') 

33 results = [] 

34 for r in resources: 

35 try: 

36 r['Tags'] = client.list_tags_for_resource( 

37 resourceArn=r['repositoryArn']).get('tags') 

38 results.append(r) 

39 except client.exceptions.RepositoryNotFoundException: 

40 continue 

41 return results 

42 

43 

44@resources.register('ecr') 

45class ECR(QueryResourceManager): 

46 

47 class resource_type(TypeInfo): 

48 service = 'ecr' 

49 enum_spec = ('describe_repositories', 'repositories', None) 

50 id = name = "repositoryName" 

51 arn = "repositoryArn" 

52 arn_type = 'repository' 

53 filter_name = 'repositoryNames' 

54 filter_type = 'list' 

55 config_type = cfn_type = 'AWS::ECR::Repository' 

56 dimension = 'RepositoryName' 

57 permissions_augment = ("ecr:ListTagsForResource",) 

58 

59 source_mapping = { 

60 'describe': DescribeECR, 

61 'config': ConfigECR 

62 } 

63 

64 

65@ECR.filter_registry.register('metrics') 

66class ECRMetricsFilter(MetricsFilter): 

67 def get_dimensions(self, resource): 

68 return [{"Name": "RepositoryName", "Value": resource['repositoryName']}] 

69 

70 

71class ECRImageQuery(ChildResourceQuery): 

72 

73 def get(self, resource_manager, identities): 

74 m = self.resolve(resource_manager.resource_type) 

75 params = {'ImageIds': [identities]} 

76 resources = self.filter(resource_manager, **params) 

77 resources = [r for r in resources if "{}/{}".format(r[0], r[1][m.id]) in identities] 

78 return resources 

79 

80 

81@sources.register('describe-ecr-image') 

82class RepositoryImageDescribeSource(ChildDescribeSource): 

83 

84 resource_query_factory = ECRImageQuery 

85 

86 def get_query(self): 

87 return super().get_query(capture_parent_id=True) 

88 

89 def get_query_params(self, query): 

90 query = query or {} 

91 if 'query' not in self.manager.data: 

92 return query 

93 for q in self.manager.data['query']: 

94 query.update(q) 

95 return query 

96 

97 def augment(self, resources): 

98 # construct an image arn 

99 ecr_manager = self.manager.get_resource_manager(self.manager.resource_type.parent_spec[0]) 

100 rtype = ecr_manager.resource_type 

101 

102 repo_arn_map = {} 

103 for repo_name in list({repo_name for repo_name, image in resources}): 

104 repo_arn_map[repo_name] = generate_arn( 

105 rtype.service, 

106 region=self.manager.config.region, 

107 account_id=self.manager.account_id, 

108 resource_type=ecr_manager.resource_type.arn_type, 

109 separator="/", 

110 resource=repo_name 

111 ) 

112 

113 results = [] 

114 for repo_name, image in resources: 

115 image['imageArn'] = "{}/{}".format(repo_arn_map[repo_name], image['imageDigest']) 

116 results.append(image) 

117 return results 

118 

119 

120@resources.register('ecr-image') 

121class RepositoryImage(ChildResourceManager): 

122 

123 class resource_type(TypeInfo): 

124 service = 'ecr' 

125 parent_spec = ('ecr', 'repositoryName', None) 

126 enum_spec = ('describe_images', 'imageDetails', None) 

127 id = 'imageDigest' 

128 name = 'repositoryName' 

129 arn = "imageArn" 

130 arn_type = 'repository' 

131 

132 source_mapping = { 

133 'describe-child': RepositoryImageDescribeSource, 

134 'describe': RepositoryImageDescribeSource, 

135 } 

136 

137 

138ECR_POLICY_SCHEMA = { 

139 'type': 'object', 

140 'properties': { 

141 'Sid': {'type': 'string'}, 

142 'Effect': {'type': 'string', 'enum': ['Allow', 'Deny']}, 

143 'Principal': {'anyOf': [ 

144 {'type': 'string'}, 

145 {'type': 'object'}, {'type': 'array'}]}, 

146 'NotPrincipal': {'anyOf': [{'type': 'object'}, {'type': 'array'}]}, 

147 'Action': {'anyOf': [{'type': 'string', 'pattern': '^ecr:([a-zA-Z]*|[*])$'}, 

148 {'type': 'array', 'items': {'type': 'string', 'pattern': '^ecr:([a-zA-Z]*|[*])$'}}]}, 

149 'NotAction': {'anyOf': [{'type': 'string', 'pattern': '^ecr:([a-zA-Z]*|[*])$'}, 

150 {'type': 'array', 'items': {'type': 'string', 'pattern': '^ecr:([a-zA-Z]*|[*])$'}}]}, 

151 'Resource': {'anyOf': [{'type': 'string'}, {'type': 'array'}]}, 

152 'NotResource': {'anyOf': [{'type': 'string'}, {'type': 'array'}]}, 

153 'Condition': {'type': 'object'} 

154 }, 

155 'required': ['Sid', 'Effect'], 

156 'oneOf': [ 

157 {'required': ['Principal', 'Action']}, 

158 {'required': ['NotPrincipal', 'Action']}, 

159 {'required': ['Principal', 'NotAction']}, 

160 {'required': ['NotPrincipal', 'NotAction']} 

161 ] 

162} 

163 

164 

165@RepositoryImage.action_registry.register('modify-ecr-policy') 

166@ECR.action_registry.register('modify-ecr-policy') 

167class ModifyPolicyStatement(ModifyPolicyBase): 

168 """Action to modify ECR policy statements. 

169 

170 :example: 

171 

172 .. code-block:: yaml 

173 

174 policies: 

175 - name: ecr-image-prevent-pull 

176 resource: ecr-image 

177 filters: 

178 - type: finding 

179 actions: 

180 - type: modify-ecr-policy 

181 add-statements: [{ 

182 "Sid": "ReplaceWithMe", 

183 "Effect": "Deny", 

184 "Principal": "*", 

185 "Action": ["ecr:BatchGetImage"] 

186 }] 

187 remove-statements: "*" 

188 """ 

189 permissions = ('ecr:GetRepositoryPolicy', 'ecr:SetRepositoryPolicy') 

190 schema = type_schema( 

191 'modify-ecr-policy', schema_alias=False, 

192 **{ 

193 'add-statements': { 

194 'type': 'array', 

195 'items': ECR_POLICY_SCHEMA, 

196 }, 

197 'remove-statements': { 

198 'type': ['array', 'string'], 

199 'oneOf': [ 

200 {'enum': ['matched', '*']}, 

201 {'type': 'array', 'items': {'type': 'string'}} 

202 ], 

203 } 

204 }) 

205 

206 def process(self, resources): 

207 results = [] 

208 client = local_session(self.manager.session_factory).client('ecr') 

209 for r in resources: 

210 try: 

211 policy = json.loads( 

212 client.get_repository_policy( 

213 repositoryName=r["repositoryName"])["policyText"]) 

214 except client.exceptions.RepositoryPolicyNotFoundException: 

215 policy = {} 

216 policy_statements = policy.setdefault('Statement', []) 

217 new_policy, removed = self.remove_statements( 

218 policy_statements, r, CrossAccountAccessFilter.annotation_key) 

219 if new_policy is None: 

220 new_policy = policy_statements 

221 new_policy, added = self.add_statements(new_policy) 

222 

223 if not removed and not added: 

224 continue 

225 elif not new_policy: 

226 client.delete_repository_policy( 

227 repositoryName=r['repositoryName']) 

228 else: 

229 cleaned = [] 

230 for statement in new_policy: 

231 if "Resource" in statement: 

232 del statement["Resource"] 

233 cleaned.append(statement) 

234 else: 

235 cleaned.append(statement) 

236 policy['Statement'] = cleaned 

237 client.set_repository_policy( 

238 repositoryName=r['repositoryName'], 

239 policyText=json.dumps(policy)) 

240 results += { 

241 'Name': r['repositoryName'], 

242 'State': 'PolicyModified', 

243 'Statements': new_policy 

244 } 

245 

246 return results 

247 

248 

249@ECR.action_registry.register('tag') 

250class ECRTag(tags.Tag): 

251 

252 permissions = ('ecr:TagResource',) 

253 

254 def process_resource_set(self, client, resources, tags): 

255 for r in resources: 

256 try: 

257 client.tag_resource(resourceArn=r['repositoryArn'], tags=tags) 

258 except client.exceptions.RepositoryNotFoundException: 

259 pass 

260 

261 

262@ECR.action_registry.register('set-scanning') 

263class ECRSetScanning(Action): 

264 

265 permissions = ('ecr:PutImageScanningConfiguration',) 

266 schema = type_schema( 

267 'set-scanning', 

268 state={'type': 'boolean', 'default': True}) 

269 

270 def process(self, resources): 

271 client = local_session(self.manager.session_factory).client('ecr') 

272 s = self.data.get('state', True) 

273 for r in resources: 

274 try: 

275 client.put_image_scanning_configuration( 

276 registryId=r['registryId'], 

277 repositoryName=r['repositoryName'], 

278 imageScanningConfiguration={ 

279 'scanOnPush': s}) 

280 except client.exceptions.RepositoryNotFoundException: 

281 continue 

282 

283 

284@ECR.action_registry.register('set-immutability') 

285class ECRSetImmutability(Action): 

286 

287 permissions = ('ecr:PutImageTagMutability',) 

288 schema = type_schema( 

289 'set-immutability', 

290 state={'type': 'boolean', 'default': True}) 

291 

292 def process(self, resources): 

293 client = local_session(self.manager.session_factory).client('ecr') 

294 s = 'IMMUTABLE' if self.data.get('state', True) else 'MUTABLE' 

295 for r in resources: 

296 try: 

297 client.put_image_tag_mutability( 

298 registryId=r['registryId'], 

299 repositoryName=r['repositoryName'], 

300 imageTagMutability=s) 

301 except client.exceptions.RepositoryNotFoundException: 

302 continue 

303 

304 

305@ECR.action_registry.register('remove-tag') 

306class ECRRemoveTags(tags.RemoveTag): 

307 

308 permissions = ('ecr:UntagResource',) 

309 

310 def process_resource_set(self, client, resources, tags): 

311 for r in resources: 

312 try: 

313 client.untag_resource(resourceArn=r['repositoryArn'], tagKeys=tags) 

314 except client.exceptions.RepositoryNotFoundException: 

315 pass 

316 

317 

318ECR.filter_registry.register('marked-for-op', tags.TagActionFilter) 

319ECR.action_registry.register('mark-for-op', tags.TagDelayedAction) 

320 

321 

322@ECR.filter_registry.register('cross-account') 

323class ECRCrossAccountAccessFilter(CrossAccountAccessFilter): 

324 """Filters all EC2 Container Registries (ECR) with cross-account access 

325 

326 :example: 

327 

328 .. code-block:: yaml 

329 

330 policies: 

331 - name: ecr-cross-account 

332 resource: ecr 

333 filters: 

334 - type: cross-account 

335 whitelist_from: 

336 expr: "accounts.*.accountNumber" 

337 url: accounts_url 

338 """ 

339 permissions = ('ecr:GetRepositoryPolicy',) 

340 

341 def process(self, resources, event=None): 

342 

343 client = local_session(self.manager.session_factory).client('ecr') 

344 

345 def _augment(r): 

346 if r.get('Policy') is not None: 

347 return r 

348 try: 

349 r['Policy'] = client.get_repository_policy( 

350 repositoryName=r['repositoryName'])['policyText'] 

351 except client.exceptions.RepositoryPolicyNotFoundException: 

352 return None 

353 return r 

354 

355 self.log.debug("fetching policy for %d repos" % len(resources)) 

356 with self.executor_factory(max_workers=2) as w: 

357 resources = list(filter(None, w.map(_augment, resources))) 

358 

359 return super(ECRCrossAccountAccessFilter, self).process(resources, event) 

360 

361 

362LIFECYCLE_RULE_SCHEMA = { 

363 'type': 'object', 

364 'additionalProperties': False, 

365 'required': ['rulePriority', 'action', 'selection'], 

366 'properties': { 

367 'rulePriority': {'type': 'integer'}, 

368 'description': {'type': 'string'}, 

369 'action': { 

370 'type': 'object', 

371 'required': ['type'], 

372 'additionalProperties': False, 

373 'properties': {'type': {'enum': ['expire']}}}, 

374 'selection': { 

375 'type': 'object', 

376 'addtionalProperties': False, 

377 'required': ['countType', 'countNumber', 'tagStatus'], 

378 'properties': { 

379 'tagStatus': {'enum': ['tagged', 'untagged', 'any']}, 

380 'tagPatternList': {'type': 'array', 'items': {'type': 'string'}}, 

381 'tagPrefixList': {'type': 'array', 'items': {'type': 'string'}}, 

382 'countNumber': {'type': 'integer'}, 

383 'countUnit': {'enum': ['hours', 'days']}, 

384 'countType': { 

385 'enum': ['imageCountMoreThan', 'sinceImagePushed']}, 

386 } 

387 } 

388 } 

389} 

390 

391 

392def lifecycle_rule_validate(policy, rule): 

393 # This is a non exhaustive list of lifecycle validation rules 

394 # see this for a more comprehensive list 

395 # 

396 # https://docs.aws.amazon.com/AmazonECR/latest/userguide/LifecyclePolicies.html#lp_evaluation_rules 

397 

398 if rule['selection']['tagStatus'] == 'tagged': 

399 if ('tagPrefixList' not in rule['selection'] and 

400 'tagPatternList' not in rule['selection']): 

401 raise PolicyValidationError( 

402 ("{} has invalid lifecycle rule {} tagPrefixList or tagPatternList " 

403 "required for tagStatus: tagged").format( 

404 policy.name, rule)) 

405 if (rule['selection']['countType'] == 'sinceImagePushed' and 

406 'countUnit' not in rule['selection']): 

407 raise PolicyValidationError( 

408 ("{} has invalid lifecycle rule {} countUnit " 

409 "required for countType: sinceImagePushed").format( 

410 policy.name, rule)) 

411 if (rule['selection']['countType'] == 'imageCountMoreThan' and 

412 'countUnit' in rule['selection']): 

413 raise PolicyValidationError( 

414 ("{} has invalid lifecycle rule {} countUnit " 

415 "invalid for countType: imageCountMoreThan").format( 

416 policy.name, rule)) 

417 

418 

419@ECR.filter_registry.register('lifecycle-rule') 

420class LifecycleRule(Filter): 

421 """Lifecycle rule filtering 

422 

423 :Example: 

424 

425 .. code-block:: yaml 

426 

427 policies: 

428 - name: ecr-life 

429 resource: aws.ecr 

430 filters: 

431 - type: lifecycle-rule 

432 state: False 

433 match: 

434 - selection.tagStatus: untagged 

435 - action.type: expire 

436 - type: value 

437 key: selection.countNumber 

438 value: 30 

439 op: less-than 

440 """ 

441 permissions = ('ecr:GetLifecyclePolicy',) 

442 schema = type_schema( 

443 'lifecycle-rule', 

444 state={'type': 'boolean'}, 

445 match={'type': 'array', 'items': { 

446 'oneOf': [ 

447 {'$ref': '#/definitions/filters/value'}, 

448 {'type': 'object', 'minProperties': 1, 'maxProperties': 1}, 

449 ]}}) 

450 policy_annotation = 'c7n:lifecycle-policy' 

451 

452 def process(self, resources, event=None): 

453 client = local_session(self.manager.session_factory).client('ecr') 

454 for r in resources: 

455 if self.policy_annotation in r: 

456 continue 

457 try: 

458 r[self.policy_annotation] = json.loads( 

459 client.get_lifecycle_policy( 

460 repositoryName=r['repositoryName']).get( 

461 'lifecyclePolicyText', '')) 

462 except client.exceptions.LifecyclePolicyNotFoundException: 

463 r[self.policy_annotation] = {} 

464 

465 state = self.data.get('state', False) 

466 matchers = [] 

467 for matcher in self.data.get('match', []): 

468 vf = ValueFilter(matcher) 

469 vf.annotate = False 

470 matchers.append(vf) 

471 

472 results = [] 

473 for r in resources: 

474 found = False 

475 for rule in r[self.policy_annotation].get('rules', []): 

476 found = True 

477 for m in matchers: 

478 if not m(rule): 

479 found = False 

480 if found and state: 

481 results.append(r) 

482 if not found and not state: 

483 results.append(r) 

484 return results 

485 

486 

487@ECR.action_registry.register('set-lifecycle') 

488class SetLifecycle(Action): 

489 """Set the lifecycle policy for ECR repositories. 

490 

491 

492 Note at the moment this is limited to set/delete/replacement of 

493 lifecycle policies, not merge. 

494 """ 

495 permissions = ('ecr:PutLifecyclePolicy', 'ecr:DeleteLifecyclePolicy') 

496 

497 schema = type_schema( 

498 'set-lifecycle', 

499 state={'type': 'boolean'}, 

500 rules={ 

501 'type': 'array', 

502 'items': LIFECYCLE_RULE_SCHEMA}) 

503 

504 def validate(self): 

505 if self.data.get('state') is False and 'rules' in self.data: 

506 raise PolicyValidationError( 

507 "set-lifecycle can't use statements and state: false") 

508 elif self.data.get('state', True) and not self.data.get('rules'): 

509 raise PolicyValidationError( 

510 "set-lifecycle requires rules with state: true") 

511 for r in self.data.get('rules', []): 

512 lifecycle_rule_validate(self.manager.ctx.policy, r) 

513 return self 

514 

515 def process(self, resources): 

516 client = local_session(self.manager.session_factory).client('ecr') 

517 state = self.data.get('state', True) 

518 for r in resources: 

519 if state is False: 

520 try: 

521 client.delete_lifecycle_policy( 

522 registryId=r['registryId'], 

523 repositoryName=r['repositoryName']) 

524 continue 

525 except client.exceptions.LifecyclePolicyNotFoundException: 

526 pass 

527 client.put_lifecycle_policy( 

528 registryId=r['registryId'], 

529 repositoryName=r['repositoryName'], 

530 lifecyclePolicyText=json.dumps({'rules': self.data['rules']})) 

531 

532 

533@ECR.action_registry.register('remove-statements') 

534class RemovePolicyStatement(RemovePolicyBase): 

535 """Action to remove policy statements from ECR 

536 

537 :example: 

538 

539 .. code-block:: yaml 

540 

541 policies: 

542 - name: ecr-remove-cross-accounts 

543 resource: ecr 

544 filters: 

545 - type: cross-account 

546 actions: 

547 - type: remove-statements 

548 statement_ids: matched 

549 """ 

550 

551 permissions = ("ecr:SetRepositoryPolicy", "ecr:GetRepositoryPolicy") 

552 

553 def process(self, resources): 

554 results = [] 

555 client = local_session(self.manager.session_factory).client('ecr') 

556 for r in resources: 

557 try: 

558 results += filter(None, [self.process_resource(client, r)]) 

559 except Exception: 

560 self.log.exception( 

561 "Error processing ecr registry:%s", r['repositoryArn']) 

562 return results 

563 

564 def process_resource(self, client, resource): 

565 if 'Policy' not in resource: 

566 try: 

567 resource['Policy'] = client.get_repository_policy( 

568 repositoryName=resource['repositoryName'])['policyText'] 

569 except client.exceptions.RepositoryPolicyNotFoundException: 

570 return 

571 

572 p = json.loads(resource['Policy']) 

573 statements, found = self.process_policy( 

574 p, resource, CrossAccountAccessFilter.annotation_key) 

575 

576 if not found: 

577 return 

578 

579 if not statements: 

580 client.delete_repository_policy( 

581 repositoryName=resource['repositoryName']) 

582 else: 

583 client.set_repository_policy( 

584 repositoryName=resource['repositoryName'], 

585 policyText=json.dumps(p)) 

586 return {'Name': resource['repositoryName'], 

587 'State': 'PolicyRemoved', 

588 'Statements': found}