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

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

14 

15 

16class DescribeECR(DescribeSource): 

17 

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 

29 

30 

31@resources.register('ecr') 

32class ECR(QueryResourceManager): 

33 

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' 

44 

45 source_mapping = { 

46 'describe': DescribeECR, 

47 'config': ConfigSource 

48 } 

49 

50 

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

52class ECRMetricsFilter(MetricsFilter): 

53 def get_dimensions(self, resource): 

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

55 

56 

57class ECRImageQuery(ChildResourceQuery): 

58 

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] 

64 

65 return resources 

66 

67 

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

69class RepositoryImageDescribeSource(ChildDescribeSource): 

70 

71 resource_query_factory = ECRImageQuery 

72 

73 def get_query(self): 

74 query = super(RepositoryImageDescribeSource, self).get_query() 

75 query.capture_parent_id = True 

76 return query 

77 

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 

88 

89 

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

91class RepositoryImage(ChildResourceManager): 

92 

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' 

101 

102 source_mapping = { 

103 'describe-child': RepositoryImageDescribeSource, 

104 'describe': RepositoryImageDescribeSource, 

105 } 

106 

107 

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} 

133 

134 

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. 

139 

140 :example: 

141 

142 .. code-block:: yaml 

143 

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 }) 

175 

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) 

192 

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 } 

215 

216 return results 

217 

218 

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

220class ECRTag(tags.Tag): 

221 

222 permissions = ('ecr:TagResource',) 

223 

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 

230 

231 

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

233class ECRSetScanning(Action): 

234 

235 permissions = ('ecr:PutImageScanningConfiguration',) 

236 schema = type_schema( 

237 'set-scanning', 

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

239 

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 

252 

253 

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

255class ECRSetImmutability(Action): 

256 

257 permissions = ('ecr:PutImageTagMutability',) 

258 schema = type_schema( 

259 'set-immutability', 

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

261 

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 

273 

274 

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

276class ECRRemoveTags(tags.RemoveTag): 

277 

278 permissions = ('ecr:UntagResource',) 

279 

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 

286 

287 

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

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

290 

291 

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

293class ECRCrossAccountAccessFilter(CrossAccountAccessFilter): 

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

295 

296 :example: 

297 

298 .. code-block:: yaml 

299 

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',) 

310 

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

312 

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

314 

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 

322 

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))) 

326 

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

328 

329 

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} 

357 

358 

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 

364 

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)) 

383 

384 

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

386class LifecycleRule(Filter): 

387 """Lifecycle rule filtering 

388 

389 :Example: 

390 

391 .. code-block:: yaml 

392 

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' 

417 

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] = {} 

430 

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) 

437 

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 

451 

452 

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

454class SetLifecycle(Action): 

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

456 

457 

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') 

462 

463 schema = type_schema( 

464 'set-lifecycle', 

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

466 rules={ 

467 'type': 'array', 

468 'items': LIFECYCLE_RULE_SCHEMA}) 

469 

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 

480 

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']})) 

497 

498 

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

500class RemovePolicyStatement(RemovePolicyBase): 

501 """Action to remove policy statements from ECR 

502 

503 :example: 

504 

505 .. code-block:: yaml 

506 

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 """ 

516 

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

518 

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 

529 

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 

537 

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

539 statements, found = self.process_policy( 

540 p, resource, CrossAccountAccessFilter.annotation_key) 

541 

542 if not found: 

543 return 

544 

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}