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

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

606 statements  

1# Copyright The Cloud Custodian Authors. 

2# SPDX-License-Identifier: Apache-2.0 

3import itertools 

4from collections import defaultdict 

5from concurrent.futures import as_completed 

6from datetime import datetime, timedelta 

7 

8import botocore.exceptions 

9from c7n import query 

10from c7n.actions import BaseAction 

11from c7n.exceptions import PolicyValidationError 

12from c7n.filters import Filter, MetricsFilter 

13from c7n.filters.core import parse_date, ValueFilter 

14from c7n.filters.iamaccess import CrossAccountAccessFilter 

15from c7n.filters.related import ChildResourceFilter 

16from c7n.filters.kms import KmsRelatedFilter 

17from c7n.query import ( 

18 QueryResourceManager, ChildResourceManager, 

19 TypeInfo, DescribeSource, ConfigSource, DescribeWithResourceTags) 

20from c7n.manager import resources 

21from c7n.resolver import ValuesFrom 

22from c7n.resources import load_resources 

23from c7n.resources.aws import ArnResolver 

24from c7n.query import RetryPageIterator 

25from c7n.tags import universal_augment 

26from c7n.utils import type_schema, local_session, chunks, get_retry, jmespath_search 

27from botocore.config import Config 

28import re 

29 

30 

31class DescribeAlarm(DescribeSource): 

32 def augment(self, resources): 

33 return universal_augment(self.manager, super().augment(resources)) 

34 

35 

36@resources.register('alarm') 

37class Alarm(QueryResourceManager): 

38 class resource_type(TypeInfo): 

39 service = 'cloudwatch' 

40 arn_type = 'alarm' 

41 enum_spec = ('describe_alarms', 'MetricAlarms', None) 

42 id = 'AlarmName' 

43 arn = 'AlarmArn' 

44 filter_name = 'AlarmNames' 

45 filter_type = 'list' 

46 name = 'AlarmName' 

47 date = 'AlarmConfigurationUpdatedTimestamp' 

48 cfn_type = config_type = 'AWS::CloudWatch::Alarm' 

49 universal_taggable = object() 

50 permissions_augment = ("cloudwatch:ListTagsForResource",) 

51 

52 source_mapping = { 

53 'describe': DescribeAlarm, 

54 'config': ConfigSource 

55 } 

56 

57 retry = staticmethod(get_retry(('Throttled',))) 

58 

59 

60@Alarm.action_registry.register('delete') 

61class AlarmDelete(BaseAction): 

62 """Delete a cloudwatch alarm. 

63 

64 :example: 

65 

66 .. code-block:: yaml 

67 

68 policies: 

69 - name: cloudwatch-delete-stale-alarms 

70 resource: alarm 

71 filters: 

72 - type: value 

73 value_type: age 

74 key: StateUpdatedTimestamp 

75 value: 30 

76 op: ge 

77 - StateValue: INSUFFICIENT_DATA 

78 actions: 

79 - delete 

80 """ 

81 

82 schema = type_schema('delete') 

83 permissions = ('cloudwatch:DeleteAlarms',) 

84 

85 def process(self, resources): 

86 client = local_session( 

87 self.manager.session_factory).client('cloudwatch') 

88 

89 for resource_set in chunks(resources, size=100): 

90 self.manager.retry( 

91 client.delete_alarms, 

92 AlarmNames=[r['AlarmName'] for r in resource_set]) 

93 

94 

95@Alarm.filter_registry.register('is-composite-child') 

96class IsCompositeChild(Filter): 

97 schema = type_schema('is-composite-child', state={"type": "boolean"}) 

98 permissions = ('cloudwatch:DescribeAlarms',) 

99 

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

101 state = self.data.get("state", True) 

102 # Get the composite alarms since filtered out in enum_spec 

103 composite_alarms = self.manager.get_resource_manager("composite-alarm").resources() 

104 composite_alarm_rules = jmespath_search('[].AlarmRule', composite_alarms) 

105 

106 child_alarm_names = set() 

107 # Loop through, find child alarm names 

108 for rule in composite_alarm_rules: 

109 names = self.extract_alarm_names_from_rule(rule) 

110 child_alarm_names.update(names) 

111 

112 if state: 

113 # If we want to filter out alarms that are a child of a composite alarm 

114 return [r for r in resources if r['AlarmName'] in child_alarm_names] 

115 

116 return [r for r in resources if r['AlarmName'] not in child_alarm_names] 

117 

118 def extract_alarm_names_from_rule(self, rule): 

119 # Check alarm references (OK/ALARM/INSUFFICIENT_DATA) 

120 pattern = r"\b(?:ALARM|OK|INSUFFICIENT_DATA)\s*\(\s*([^\)]+)\s*\)" 

121 matches = re.findall(pattern, rule) 

122 return set(matches) 

123 

124 

125@resources.register('composite-alarm') 

126class CompositeAlarm(QueryResourceManager): 

127 

128 class resource_type(TypeInfo): 

129 service = 'cloudwatch' 

130 arn_type = 'alarm' 

131 enum_spec = ('describe_alarms', 'CompositeAlarms', {'AlarmTypes': ['CompositeAlarm']}) 

132 id = name = 'AlarmName' 

133 arn = 'AlarmArn' 

134 date = 'AlarmConfigurationUpdatedTimestamp' 

135 cfn_type = 'AWS::CloudWatch::CompositeAlarm' 

136 universal_taggable = object() 

137 

138 augment = universal_augment 

139 

140 retry = staticmethod(get_retry(('Throttled',))) 

141 

142 

143@CompositeAlarm.action_registry.register('delete') 

144class CompositeAlarmDelete(BaseAction): 

145 """Delete a cloudwatch composite alarm. 

146 

147 :example: 

148 

149 .. code-block:: yaml 

150 

151 policies: 

152 - name: cloudwatch-delete-composite-alarms 

153 resource: aws.composite-alarm 

154 filters: 

155 - type: value 

156 value_type: age 

157 key: StateUpdatedTimestamp 

158 value: 30 

159 op: ge 

160 - StateValue: INSUFFICIENT_DATA 

161 actions: 

162 - delete 

163 """ 

164 

165 schema = type_schema('delete') 

166 permissions = ('cloudwatch:DeleteAlarms',) 

167 

168 def process(self, resources): 

169 client = local_session( 

170 self.manager.session_factory).client('cloudwatch') 

171 

172 for resource_set in chunks(resources, size=100): 

173 self.manager.retry( 

174 client.delete_alarms, 

175 AlarmNames=[r['AlarmName'] for r in resource_set]) 

176 

177 

178@resources.register('event-bus') 

179class EventBus(QueryResourceManager): 

180 class resource_type(TypeInfo): 

181 service = 'events' 

182 arn_type = 'event-bus' 

183 arn = 'Arn' 

184 enum_spec = ('list_event_buses', 'EventBuses', None) 

185 detail_spec = ('describe_event_bus', 'Name', 'Name', None) 

186 config_type = cfn_type = 'AWS::Events::EventBus' 

187 id = name = 'Name' 

188 universal_taggable = object() 

189 permissions_augment = ("events:ListTagsForResource",) 

190 

191 source_mapping = {'describe': DescribeWithResourceTags, 

192 'config': ConfigSource} 

193 

194 

195@EventBus.filter_registry.register('cross-account') 

196class EventBusCrossAccountFilter(CrossAccountAccessFilter): 

197 # dummy permission 

198 permissions = ('events:ListEventBuses',) 

199 

200 

201@EventBus.filter_registry.register('kms-key') 

202class EventBusKmsFilter(KmsRelatedFilter): 

203 RelatedIdsExpression = 'KmsKeyIdentifier' 

204 

205 

206@EventBus.action_registry.register('delete') 

207class EventBusDelete(BaseAction): 

208 """Delete an event bus. 

209 

210 :example: 

211 

212 .. code-block:: yaml 

213 

214 policies: 

215 - name: cloudwatch-delete-event-bus 

216 resource: aws.event-bus 

217 filters: 

218 - Name: test-event-bus 

219 actions: 

220 - delete 

221 """ 

222 

223 schema = type_schema('delete') 

224 permissions = ('events:DeleteEventBus',) 

225 

226 def process(self, resources): 

227 client = local_session( 

228 self.manager.session_factory).client('events') 

229 

230 for resource_set in chunks(resources, size=100): 

231 for r in resource_set: 

232 self.manager.retry( 

233 client.delete_event_bus, 

234 Name=r['Name']) 

235 

236 

237class EventRuleQuery(query.ChildResourceQuery): 

238 

239 def get_parent_parameters(self, params, parent_id, parent_key): 

240 merged_params = dict(params) 

241 merged_params[parent_key] = parent_id 

242 return merged_params 

243 

244 

245@query.sources.register('event-rule') 

246class EventRuleSource(query.ChildDescribeSource): 

247 

248 resource_query_factory = EventRuleQuery 

249 

250 def augment(self, resources): 

251 return universal_augment(self.manager, resources) 

252 

253 

254@resources.register('event-rule') 

255class EventRule(ChildResourceManager): 

256 

257 child_source = 'event-rule' 

258 class resource_type(TypeInfo): 

259 service = 'events' 

260 arn = 'Arn' 

261 enum_spec = ('list_rules', 'Rules', None) 

262 parent_spec = ('event-bus', 'EventBusName', None) 

263 name = "Name" 

264 id = "Name" 

265 filter_name = "NamePrefix" 

266 filter_type = "scalar" 

267 config_type = cfn_type = 'AWS::Events::Rule' 

268 universal_taggable = object() 

269 permissions_augment = ("events:ListTagsForResource",) 

270 

271 

272@EventRule.filter_registry.register('metrics') 

273class EventRuleMetrics(MetricsFilter): 

274 

275 def get_dimensions(self, resource): 

276 return [{'Name': 'RuleName', 'Value': resource['Name']}] 

277 

278 

279class EventChildResourceFilter(ChildResourceFilter): 

280 

281 # This function provides custom functionality to query event-rule-targets 

282 # using both event-rule and event-bus information. 

283 def get_related(self, resources): 

284 self.child_resources = {} 

285 child_resource_manager = self.get_resource_manager() 

286 client = local_session(child_resource_manager.session_factory).client('events') 

287 paginator_targets = client.get_paginator('list_targets_by_rule') 

288 paginator_targets.PAGE_ITERATOR_CLS = RetryPageIterator 

289 

290 for r in resources: 

291 targets = paginator_targets.paginate(EventBusName=r['EventBusName'], Rule=r['Name']) \ 

292 .build_full_result().get('Targets', []) 

293 for target in targets: 

294 target[self.ChildResourceParentKey] = r['Name'] 

295 self.child_resources.setdefault(target[self.ChildResourceParentKey], []) \ 

296 .append(target) 

297 

298 return self.child_resources 

299 

300 

301@EventRule.filter_registry.register('event-rule-target') 

302class EventRuleTargetFilter(EventChildResourceFilter): 

303 

304 """ 

305 Filter event rules by their targets 

306 

307 :example: 

308 

309 .. code-block:: yaml 

310 

311 policies: 

312 - name: find-event-rules-with-no-targets 

313 resource: aws.event-rule 

314 filters: 

315 - type: event-rule-target 

316 key: "@" 

317 value: empty 

318 

319 - name: find-event-rules-by-target-properties 

320 resource: aws.event-rule 

321 filters: 

322 - type: event-rule-target 

323 key: "[].Arn" 

324 op: contains 

325 value: "arn:aws:sqs:us-east-2:111111111111:my-queue" 

326 """ 

327 

328 RelatedResource = "c7n.resources.cw.EventRuleTarget" 

329 RelatedIdsExpression = 'Name' 

330 AnnotationKey = "EventRuleTargets" 

331 

332 schema = type_schema('event-rule-target', rinherit=ValueFilter.schema) 

333 permissions = ('events:ListTargetsByRule',) 

334 

335 

336@EventRule.filter_registry.register('invalid-targets') 

337class ValidEventRuleTargetFilter(EventChildResourceFilter): 

338 """ 

339 Filter event rules for invalid targets, Use the `all` option to 

340 find any event rules that have all invalid targets, otherwise 

341 defaults to filtering any event rule with at least one invalid 

342 target. 

343 

344 :example: 

345 

346 .. code-block:: yaml 

347 

348 policies: 

349 - name: find-event-rules-with-invalid-targets 

350 resource: aws.event-rule 

351 filters: 

352 - type: invalid-targets 

353 all: true # defaults to false 

354 """ 

355 

356 RelatedResource = "c7n.resources.cw.EventRuleTarget" 

357 RelatedIdsExpression = 'Name' 

358 AnnotationKey = "EventRuleTargets" 

359 

360 schema = type_schema( 

361 'invalid-targets', 

362 **{ 

363 'all': { 

364 'type': 'boolean', 

365 'default': False 

366 } 

367 } 

368 ) 

369 

370 permissions = ('events:ListTargetsByRule',) 

371 supported_resources = ( 

372 "aws.sqs", 

373 "aws.event-bus", 

374 "aws.lambda", 

375 "aws.ecs", 

376 "aws.ecs-task", 

377 "aws.kinesis", 

378 "aws.sns", 

379 "aws.ssm-parameter", 

380 "aws.batch-compute", 

381 "aws.codepipeline", 

382 "aws.step-machine", 

383 ) 

384 

385 def validate(self): 

386 """ 

387 Empty validate here to bypass the validation found in the base value filter 

388 as we're inheriting from the ChildResourceFilter/RelatedResourceFilter 

389 """ 

390 return self 

391 

392 def get_rules_with_children(self, resources): 

393 """ 

394 Augments resources by adding the c7n:ChildArns to the resource dict 

395 """ 

396 

397 results = [] 

398 

399 # returns a map of {parent_reosurce_id: [{child_resource}, {child_resource2}, etc.]} 

400 child_resources = self.get_related(resources) 

401 

402 # maps resources by their name to their data 

403 for r in resources: 

404 if child_resources.get(r['Name']): 

405 for c in child_resources[r['Name']]: 

406 r.setdefault('c7n:ChildArns', []).append(c['Arn']) 

407 results.append(r) 

408 return results 

409 

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

411 # Due to lazy loading of resources, we need to explicilty load the following 

412 # potential targets for a event rule target: 

413 

414 load_resources(list(self.supported_resources)) 

415 arn_resolver = ArnResolver(self.manager) 

416 resources = self.get_rules_with_children(resources) 

417 resources = [r for r in resources if self.filter_unsupported_resources(r)] 

418 results = [] 

419 

420 if self.data.get('all'): 

421 op = any 

422 else: 

423 op = all 

424 

425 for r in resources: 

426 resolved = arn_resolver.resolve(r['c7n:ChildArns']) 

427 if not op(resolved.values()): 

428 for i, j in resolved.items(): 

429 if not j: 

430 r.setdefault('c7n:InvalidTargets', []).append(i) 

431 results.append(r) 

432 return results 

433 

434 def filter_unsupported_resources(self, r): 

435 for carn in r.get('c7n:ChildArns'): 

436 if 'aws.' + str(ArnResolver.resolve_type(carn)) not in self.supported_resources: 

437 self.log.info( 

438 f"Skipping resource {r.get('Arn')}, target type {carn} is not supported") 

439 return False 

440 return True 

441 

442 

443@EventRule.action_registry.register('delete') 

444class EventRuleDelete(BaseAction): 

445 """ 

446 Delete an event rule, force target removal with the `force` option 

447 

448 :example: 

449 

450 .. code-block:: yaml 

451 

452 policies: 

453 - name: force-delete-rules 

454 resource: aws.event-rule 

455 filters: 

456 - Name: my-event-rule 

457 actions: 

458 - type: delete 

459 force: true 

460 """ 

461 

462 schema = type_schema('delete', force={'type': 'boolean'}) 

463 permissions = ('events:DeleteRule', 'events:RemoveTargets', 'events:ListTargetsByRule',) 

464 

465 def process(self, resources): 

466 client = local_session(self.manager.session_factory).client('events') 

467 children = {} 

468 target_error_msg = "Rule can't be deleted since it has targets." 

469 for r in resources: 

470 try: 

471 client.delete_rule(Name=r['Name'], EventBusName=r['EventBusName']) 

472 except botocore.exceptions.ClientError as e: 

473 if e.response['Error']['Message'] != target_error_msg: 

474 raise 

475 if not self.data.get('force'): 

476 self.log.warning( 

477 'Unable to delete %s event rule due to attached rule targets,' 

478 'set force to true to remove targets' % r['Name']) 

479 raise 

480 child_manager = self.manager.get_resource_manager('aws.event-rule-target') 

481 if not children: 

482 children = EventRuleTargetFilter({}, child_manager).get_related(resources) 

483 targets = list(set([t['Id'] for t in children.get(r['Name'])])) 

484 client.remove_targets(Rule=r['Name'], Ids=targets, EventBusName=r['EventBusName']) 

485 client.delete_rule(Name=r['Name'], EventBusName=r['EventBusName']) 

486 

487 

488@EventRule.action_registry.register('set-rule-state') 

489class SetRuleState(BaseAction): 

490 """ 

491 This action allows to enable/disable a rule 

492 

493 :example: 

494 

495 .. code-block:: yaml 

496 

497 policies: 

498 - name: test-rule 

499 resource: aws.event-rule 

500 filters: 

501 - Name: my-event-rule 

502 actions: 

503 - type: set-rule-state 

504 enabled: true 

505 """ 

506 

507 schema = type_schema( 

508 'set-rule-state', 

509 **{'enabled': {'default': True, 'type': 'boolean'}} 

510 ) 

511 permissions = ('events:EnableRule', 'events:DisableRule',) 

512 

513 def process(self, resources): 

514 config = Config( 

515 retries={ 

516 'max_attempts': 8, 

517 'mode': 'standard' 

518 } 

519 ) 

520 client = local_session(self.manager.session_factory).client('events', config=config) 

521 retry = get_retry(('TooManyRequestsException', 'ResourceConflictException')) 

522 enabled = self.data.get('enabled') 

523 for resource in resources: 

524 try: 

525 if enabled: 

526 retry( 

527 client.enable_rule, 

528 Name=resource['Name'] 

529 ) 

530 else: 

531 retry( 

532 client.disable_rule, 

533 Name=resource['Name'] 

534 ) 

535 except (client.exceptions.ResourceNotFoundException, 

536 client.exceptions.ManagedRuleException): 

537 continue 

538 

539 

540class EventRuleTargetQuery(query.ChildResourceQuery): 

541 

542 # This function provides custom functionality to query event-rule-targets 

543 # using both event-rule and event-bus information. 

544 def filter(self, resource_manager, parent_ids=None, **params): 

545 """Query a set of resources.""" 

546 m = self.resolve(resource_manager.resource_type) 

547 client = local_session(self.session_factory).client(m.service) 

548 

549 enum_op, path, extra_args = m.enum_spec 

550 if extra_args: 

551 params.update(extra_args) 

552 

553 parent_type, parent_key, annotate_parent = m.parent_spec 

554 parents = self.manager.get_resource_manager(parent_type) 

555 parent_resources = [] 

556 for p in parents.resources(augment=False): 

557 parent_resources.append((p)) 

558 

559 # Have to query separately for each parent's children. 

560 results = [] 

561 for parent in parent_resources: 

562 params['EventBusName'] = parent['EventBusName'] 

563 merged_params = self.get_parent_parameters(params, parent['Name'], parent_key) 

564 subset = self._invoke_client_enum( 

565 client, enum_op, merged_params, path, retry=self.manager.retry) 

566 if annotate_parent: 

567 for r in subset: 

568 r[self.parent_key] = parent['Name'] 

569 r[parent_key] = parent 

570 if subset: 

571 results.extend(subset) 

572 return results 

573 

574 def get_parent_parameters(self, params, parent_id, parent_key): 

575 merged_params = dict(params) 

576 merged_params[parent_key] = parent_id 

577 return merged_params 

578 

579 

580@query.sources.register('event-rule-target') 

581class EventRuleTargetSource(query.ChildDescribeSource): 

582 

583 resource_query_factory = EventRuleTargetQuery 

584 

585 

586@resources.register('event-rule-target') 

587class EventRuleTarget(ChildResourceManager): 

588 

589 child_source = 'event-rule-target' 

590 class resource_type(TypeInfo): 

591 service = 'events' 

592 arn = False 

593 arn_type = 'event-rule-target' 

594 enum_spec = ('list_targets_by_rule', 'Targets', None) 

595 parent_spec = ('event-rule', 'Rule', True) 

596 name = id = 'Id' 

597 

598 

599@EventRuleTarget.filter_registry.register('cross-account') 

600class CrossAccountFilter(CrossAccountAccessFilter): 

601 schema = type_schema( 

602 'cross-account', 

603 # white list accounts 

604 whitelist_from=ValuesFrom.schema, 

605 whitelist={'type': 'array', 'items': {'type': 'string'}}) 

606 

607 # dummy permission 

608 permissions = ('events:ListTargetsByRule',) 

609 

610 def __call__(self, r): 

611 account_id = r['Arn'].split(':', 5)[4] 

612 return account_id not in self.accounts 

613 

614 

615@EventRuleTarget.action_registry.register('delete') 

616class DeleteTarget(BaseAction): 

617 schema = type_schema('delete') 

618 permissions = ('events:RemoveTargets',) 

619 

620 def process(self, resources): 

621 client = local_session(self.manager.session_factory).client('events') 

622 rule_targets = {} 

623 for r in resources: 

624 event_bus = r['Rule']['EventBusName'] 

625 rule_id = r['c7n:parent-id'] 

626 rule_targets.setdefault((rule_id, event_bus), []).append(r['Id']) 

627 

628 for (rule_id, event_bus), target_ids in rule_targets.items(): 

629 client.remove_targets( 

630 Ids=target_ids, 

631 Rule=rule_id, 

632 EventBusName=event_bus) 

633 

634 

635@resources.register('log-group') 

636class LogGroup(QueryResourceManager): 

637 class resource_type(TypeInfo): 

638 service = 'logs' 

639 arn_type = 'log-group' 

640 enum_spec = ('describe_log_groups', 'logGroups', None) 

641 id = name = 'logGroupName' 

642 arn = 'arn' # see get-arns override re attribute usage 

643 filter_name = 'logGroupNamePrefix' 

644 filter_type = 'scalar' 

645 dimension = 'LogGroupName' 

646 date = 'creationTime' 

647 universal_taggable = True 

648 cfn_type = 'AWS::Logs::LogGroup' 

649 permissions_augment = ("logs:ListTagsForResource",) 

650 

651 augment = universal_augment 

652 

653 def get_arns(self, resources): 

654 # log group arn in resource describe has ':*' suffix, not all 

655 # apis can use that form, so normalize to standard arn. 

656 return [r['arn'][:-2] for r in resources] 

657 

658 

659@resources.register('insight-rule') 

660class InsightRule(QueryResourceManager): 

661 class resource_type(TypeInfo): 

662 service = 'cloudwatch' 

663 arn_type = 'insight-rule' 

664 enum_spec = ('describe_insight_rules', 'InsightRules', None) 

665 name = id = 'Name' 

666 universal_taggable = object() 

667 permission_augment = ('cloudWatch::ListTagsForResource',) 

668 cfn_type = 'AWS::CloudWatch::InsightRule' 

669 

670 def augment(self, rules): 

671 client = local_session(self.session_factory).client('cloudwatch') 

672 

673 def _add_tags(r): 

674 arn = self.generate_arn(r['Name']) 

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

676 ResourceARN=arn).get('Tags', []) 

677 return r 

678 

679 return list(map(_add_tags, rules)) 

680 

681 

682@InsightRule.action_registry.register('disable') 

683class InsightRuleDisable(BaseAction): 

684 """Disable a cloudwatch contributor insight rule. 

685 

686 :example: 

687 

688 .. code-block:: yaml 

689 

690 policies: 

691 - name: cloudwatch-disable-insight-rule 

692 resource: insight-rule 

693 filters: 

694 - type: value 

695 key: State 

696 value: ENABLED 

697 op: eq 

698 actions: 

699 - disable 

700 """ 

701 

702 schema = type_schema('disable') 

703 permissions = ('cloudwatch:DisableInsightRules',) 

704 

705 def process(self, resources): 

706 client = local_session( 

707 self.manager.session_factory).client('cloudwatch') 

708 

709 for resource_set in chunks(resources, size=100): 

710 self.manager.retry( 

711 client.disable_insight_rules, 

712 RuleNames=[r['Name'] for r in resource_set]) 

713 

714 

715@InsightRule.action_registry.register('delete') 

716class InsightRuleDelete(BaseAction): 

717 """Delete a cloudwatch contributor insight rule 

718 

719 :example: 

720 

721 .. code-block:: yaml 

722 

723 policies: 

724 - name: cloudwatch-delete-insight-rule 

725 resource: insight-rule 

726 filters: 

727 - type: value 

728 key: State 

729 value: ENABLED 

730 op: eq 

731 actions: 

732 - delete 

733 """ 

734 

735 schema = type_schema('delete') 

736 permissions = ('cloudwatch:DeleteInsightRules',) 

737 

738 def process(self, resources): 

739 client = local_session( 

740 self.manager.session_factory).client('cloudwatch') 

741 

742 for resource_set in chunks(resources, size=100): 

743 self.manager.retry( 

744 client.delete_insight_rules, 

745 RuleNames=[r['Name'] for r in resource_set]) 

746 

747 

748@LogGroup.filter_registry.register('metrics') 

749class LogGroupMetrics(MetricsFilter): 

750 

751 def get_dimensions(self, resource): 

752 return [{'Name': 'LogGroupName', 'Value': resource['logGroupName']}] 

753 

754 

755@resources.register('log-metric') 

756class LogMetric(QueryResourceManager): 

757 class resource_type(TypeInfo): 

758 service = 'logs' 

759 enum_spec = ('describe_metric_filters', 'metricFilters', None) 

760 arn = False 

761 id = name = 'filterName' 

762 date = 'creationTime' 

763 cfn_type = 'AWS::Logs::MetricFilter' 

764 

765 

766@LogMetric.filter_registry.register('alarm') 

767class LogMetricAlarmFilter(ValueFilter): 

768 """ 

769 Filter log metric filters based on associated alarms. 

770 

771 :example: 

772 

773 .. code-block:: yaml 

774 

775 policies: 

776 - name: log-metrics-with-alarms 

777 resource: aws.log-metric 

778 filters: 

779 - type: alarm 

780 key: AlarmName 

781 value: present 

782 """ 

783 

784 schema = type_schema('alarm', rinherit=ValueFilter.schema) 

785 annotation_key = 'c7n:MetricAlarms' 

786 FetchThreshold = 10 # below this number of resources, fetch alarms individually 

787 

788 def augment(self, resources): 

789 """Add alarm details to log metric filter resources 

790 

791 This includes all alarms where the metric name and namespace match 

792 a log metric filter's metric transformation. 

793 """ 

794 

795 if len(resources) < self.FetchThreshold: 

796 client = local_session(self.manager.session_factory).client('cloudwatch') 

797 for r in resources: 

798 r[self.annotation_key] = list(itertools.chain(*( 

799 self.manager.retry( 

800 client.describe_alarms_for_metric, 

801 Namespace=t['metricNamespace'], 

802 MetricName=t['metricName'])['MetricAlarms'] 

803 for t in r.get('metricTransformations', ()) 

804 ))) 

805 else: 

806 alarms = self.manager.get_resource_manager('aws.alarm').resources() 

807 

808 # We'll be matching resources to alarms based on namespace and 

809 # metric name - this lookup table makes that smoother 

810 alarms_by_metric = defaultdict(list) 

811 for alarm in alarms: 

812 alarms_by_metric[(alarm['Namespace'], alarm['MetricName'])].append(alarm) 

813 

814 for r in resources: 

815 r[self.annotation_key] = list(itertools.chain(*( 

816 alarms_by_metric.get((t['metricNamespace'], t['metricName']), []) 

817 for t in r.get('metricTransformations', ()) 

818 ))) 

819 

820 def get_permissions(self): 

821 return [ 

822 *self.manager.get_resource_manager('aws.alarm').get_permissions(), 

823 'cloudwatch:DescribeAlarmsForMetric' 

824 ] 

825 

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

827 self.augment(resources) 

828 

829 matched = [] 

830 for r in resources: 

831 if any((self.match(alarm) for alarm in r[self.annotation_key])): 

832 matched.append(r) 

833 return matched 

834 

835 

836@LogGroup.action_registry.register('retention') 

837class Retention(BaseAction): 

838 """Action to set the retention period (in days) for CloudWatch log groups 

839 

840 :example: 

841 

842 .. code-block:: yaml 

843 

844 policies: 

845 - name: cloudwatch-set-log-group-retention 

846 resource: log-group 

847 actions: 

848 - type: retention 

849 days: 200 

850 """ 

851 

852 schema = type_schema('retention', days={'type': 'integer'}) 

853 permissions = ('logs:PutRetentionPolicy',) 

854 

855 def process(self, resources): 

856 client = local_session(self.manager.session_factory).client('logs') 

857 days = self.data['days'] 

858 for r in resources: 

859 self.manager.retry( 

860 client.put_retention_policy, 

861 logGroupName=r['logGroupName'], 

862 retentionInDays=days) 

863 

864 

865@LogGroup.action_registry.register('delete') 

866class Delete(BaseAction): 

867 """ 

868 

869 :example: 

870 

871 .. code-block:: yaml 

872 

873 policies: 

874 - name: cloudwatch-delete-stale-log-group 

875 resource: log-group 

876 filters: 

877 - type: last-write 

878 days: 182.5 

879 actions: 

880 - delete 

881 """ 

882 

883 schema = type_schema('delete') 

884 permissions = ('logs:DeleteLogGroup',) 

885 

886 def process(self, resources): 

887 client = local_session(self.manager.session_factory).client('logs') 

888 for r in resources: 

889 try: 

890 self.manager.retry( 

891 client.delete_log_group, logGroupName=r['logGroupName']) 

892 except client.exceptions.ResourceNotFoundException: 

893 continue 

894 

895 

896@LogGroup.filter_registry.register('last-write') 

897class LastWriteDays(Filter): 

898 """Filters CloudWatch log groups by last write 

899 

900 :example: 

901 

902 .. code-block:: yaml 

903 

904 policies: 

905 - name: cloudwatch-stale-groups 

906 resource: log-group 

907 filters: 

908 - type: last-write 

909 days: 60 

910 """ 

911 

912 schema = type_schema( 

913 'last-write', days={'type': 'number'}) 

914 permissions = ('logs:DescribeLogStreams',) 

915 

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

917 client = local_session(self.manager.session_factory).client('logs') 

918 self.date_threshold = parse_date(datetime.utcnow()) - timedelta( 

919 days=self.data['days']) 

920 return [r for r in resources if self.check_group(client, r)] 

921 

922 def check_group(self, client, group): 

923 streams = self.manager.retry( 

924 client.describe_log_streams, 

925 logGroupName=group['logGroupName'], 

926 orderBy='LastEventTime', 

927 descending=True, 

928 limit=3).get('logStreams') 

929 group['streams'] = streams 

930 if not streams: 

931 last_timestamp = group['creationTime'] 

932 elif 'lastIngestionTime' in streams[0]: 

933 last_timestamp = streams[0]['lastIngestionTime'] 

934 else: 

935 last_timestamp = streams[0]['creationTime'] 

936 

937 last_write = parse_date(last_timestamp) 

938 group['lastWrite'] = last_write 

939 return self.date_threshold > last_write 

940 

941 

942@LogGroup.filter_registry.register('cross-account') 

943class LogCrossAccountFilter(CrossAccountAccessFilter): 

944 schema = type_schema( 

945 'cross-account', 

946 # white list accounts 

947 whitelist_from=ValuesFrom.schema, 

948 whitelist={'type': 'array', 'items': {'type': 'string'}}) 

949 

950 permissions = ('logs:DescribeSubscriptionFilters',) 

951 

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

953 client = local_session(self.manager.session_factory).client('logs') 

954 accounts = self.get_accounts() 

955 results = [] 

956 with self.executor_factory(max_workers=1) as w: 

957 futures = [] 

958 for rset in chunks(resources, 50): 

959 futures.append( 

960 w.submit( 

961 self.process_resource_set, client, accounts, rset)) 

962 for f in as_completed(futures): 

963 if f.exception(): 

964 self.log.error( 

965 "Error checking log groups cross-account %s", 

966 f.exception()) 

967 continue 

968 results.extend(f.result()) 

969 return results 

970 

971 def process_resource_set(self, client, accounts, resources): 

972 results = [] 

973 for r in resources: 

974 found = False 

975 filters = self.manager.retry( 

976 client.describe_subscription_filters, 

977 logGroupName=r['logGroupName']).get('subscriptionFilters', ()) 

978 for f in filters: 

979 if 'destinationArn' not in f: 

980 continue 

981 account_id = f['destinationArn'].split(':', 5)[4] 

982 if account_id not in accounts: 

983 r.setdefault('c7n:CrossAccountViolations', []).append( 

984 account_id) 

985 found = True 

986 if found: 

987 results.append(r) 

988 return results 

989 

990 

991@LogGroup.filter_registry.register('subscription-filter') 

992class LogSubscriptionFilter(ValueFilter): 

993 """Filters CloudWatch log groups by subscriptions 

994 

995 :example: 

996 

997 .. code-block:: yaml 

998 

999 policies: 

1000 - name: cloudwatch-groups-with-subscriptions 

1001 resource: log-group 

1002 filters: 

1003 - type: subscription-filter 

1004 key: destinationArn 

1005 value: arn:aws:lambda:us-east-1:123456789876:function:forwarder 

1006 """ 

1007 schema = type_schema('subscription-filter', rinherit=ValueFilter.schema) 

1008 annotation_key = 'c7n:SubscriptionFilters' 

1009 permissions = ('logs:DescribeSubscriptionFilters',) 

1010 

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

1012 client = local_session(self.manager.session_factory).client('logs') 

1013 results = [] 

1014 for r in resources: 

1015 filters = self.manager.retry( 

1016 client.describe_subscription_filters, 

1017 logGroupName=r['logGroupName']).get('subscriptionFilters', ()) 

1018 if not any(filters): 

1019 continue 

1020 for f in filters: 

1021 r.setdefault(self.annotation_key, []).append(f) 

1022 if (len(self.data) == 1) or any((self.match(sub) for sub in r[self.annotation_key])): 

1023 results.append(r) 

1024 return results 

1025 

1026 

1027@LogGroup.filter_registry.register('kms-key') 

1028class KmsFilter(KmsRelatedFilter): 

1029 RelatedIdsExpression = 'kmsKeyId' 

1030 

1031 

1032@LogGroup.action_registry.register('set-encryption') 

1033class EncryptLogGroup(BaseAction): 

1034 """Encrypt/Decrypt a log group 

1035 

1036 :example: 

1037 

1038 .. code-block:: yaml 

1039 

1040 policies: 

1041 - name: encrypt-log-group 

1042 resource: log-group 

1043 filters: 

1044 - kmsKeyId: absent 

1045 actions: 

1046 - type: set-encryption 

1047 kms-key: alias/mylogkey 

1048 state: True 

1049 

1050 - name: decrypt-log-group 

1051 resource: log-group 

1052 filters: 

1053 - kmsKeyId: kms:key:arn 

1054 actions: 

1055 - type: set-encryption 

1056 state: False 

1057 """ 

1058 schema = type_schema( 

1059 'set-encryption', 

1060 **{'kms-key': {'type': 'string'}, 

1061 'state': {'type': 'boolean'}}) 

1062 permissions = ( 

1063 'logs:AssociateKmsKey', 'logs:DisassociateKmsKey', 'kms:DescribeKey') 

1064 

1065 def validate(self): 

1066 if not self.data.get('state', True): 

1067 return self 

1068 key = self.data.get('kms-key', '') 

1069 if not key: 

1070 raise ValueError('Must specify either a KMS key ARN or Alias') 

1071 if 'alias/' not in key and ':key/' not in key: 

1072 raise PolicyValidationError( 

1073 "Invalid kms key format %s" % key) 

1074 return self 

1075 

1076 def resolve_key(self, key): 

1077 if not key: 

1078 return 

1079 

1080 # Qualified arn for key 

1081 if key.startswith('arn:') and ':key/' in key: 

1082 return key 

1083 

1084 # Alias 

1085 key = local_session( 

1086 self.manager.session_factory).client( 

1087 'kms').describe_key( 

1088 KeyId=key)['KeyMetadata']['Arn'] 

1089 return key 

1090 

1091 def process(self, resources): 

1092 session = local_session(self.manager.session_factory) 

1093 client = session.client('logs') 

1094 

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

1096 key = self.resolve_key(self.data.get('kms-key')) 

1097 

1098 for r in resources: 

1099 try: 

1100 if state: 

1101 client.associate_kms_key( 

1102 logGroupName=r['logGroupName'], kmsKeyId=key) 

1103 else: 

1104 client.disassociate_kms_key(logGroupName=r['logGroupName']) 

1105 except client.exceptions.ResourceNotFoundException: 

1106 continue 

1107 

1108 

1109@LogGroup.action_registry.register('put-subscription-filter') 

1110class SubscriptionFilter(BaseAction): 

1111 """Create/Update a subscription filter and associate with a log group 

1112 

1113 :example: 

1114 

1115 .. code-block:: yaml 

1116 

1117 policies: 

1118 - name: cloudwatch-put-subscription-filter 

1119 resource: log-group 

1120 actions: 

1121 - type: put-subscription-filter 

1122 filter_name: AllLambda 

1123 filter_pattern: ip 

1124 destination_arn: arn:aws:logs:us-east-1:1234567890:destination:lambda 

1125 distribution: Random 

1126 role_arn: "arn:aws:iam::{account_id}:role/testCrossAccountRole" 

1127 """ 

1128 schema = type_schema( 

1129 'put-subscription-filter', 

1130 filter_name={'type': 'string'}, 

1131 filter_pattern={'type': 'string'}, 

1132 destination_arn={'type': 'string'}, 

1133 distribution={'enum': ['Random', 'ByLogStream']}, 

1134 role_arn={'type': 'string'}, 

1135 required=['filter_name', 'destination_arn']) 

1136 permissions = ('logs:PutSubscriptionFilter',) 

1137 

1138 def process(self, resources): 

1139 session = local_session(self.manager.session_factory) 

1140 client = session.client('logs') 

1141 params = dict( 

1142 filterName=self.data.get('filter_name'), 

1143 filterPattern=self.data.get('filter_pattern', ''), 

1144 destinationArn=self.data.get('destination_arn'), 

1145 distribution=self.data.get('distribution', 'ByLogStream')) 

1146 

1147 if self.data.get('role_arn'): 

1148 params['roleArn'] = self.data.get('role_arn') 

1149 

1150 for r in resources: 

1151 client.put_subscription_filter( 

1152 logGroupName=r['logGroupName'], **params) 

1153 

1154 

1155@resources.register("cloudwatch-dashboard") 

1156class CloudWatchDashboard(QueryResourceManager): 

1157 class resource_type(TypeInfo): 

1158 service = "cloudwatch" 

1159 enum_spec = ('list_dashboards', 'DashboardEntries', None) 

1160 arn_type = "dashboard" 

1161 arn = "DashboardArn" 

1162 id = "DashboardName" 

1163 name = "DashboardName" 

1164 cfn_type = "AWS::CloudWatch::Dashboard" 

1165 universal_taggable = object() 

1166 global_resource = True 

1167 

1168 source_mapping = { 

1169 "describe": DescribeWithResourceTags, 

1170 } 

1171 

1172 

1173@resources.register("destination") 

1174class Destination(QueryResourceManager): 

1175 class resource_type(TypeInfo): 

1176 service = "logs" 

1177 arn = "arn" 

1178 arn_separator = ":" 

1179 arn_type = "destination" 

1180 cfn_type = "AWS::Logs::Destination" 

1181 date = "creationTime" 

1182 enum_spec = ('describe_destinations', 'destinations', None) 

1183 id = name = "destinationName" 

1184 universal_taggable = object() 

1185 

1186 retry = staticmethod(get_retry(('ServiceUnavailableException', 'OperationAbortedException'))) 

1187 

1188 source_mapping = { 

1189 "describe": DescribeWithResourceTags, 

1190 } 

1191 

1192 

1193@Destination.filter_registry.register('cross-account') 

1194class DestinationCrossAccount(CrossAccountAccessFilter): 

1195 

1196 permissions = ('logs:DescribeDestinations',) 

1197 policy_attribute = 'accessPolicy' 

1198 

1199 

1200@Destination.action_registry.register('delete') 

1201class DestinationDelete(BaseAction): 

1202 """Action to delete a destination 

1203 

1204 :example: 

1205 

1206 .. code-block:: yaml 

1207 

1208 policies: 

1209 - name: delete-destination 

1210 resource: aws.destination 

1211 filters: 

1212 - type: cross-account 

1213 actions: 

1214 - delete 

1215 """ 

1216 schema = type_schema('delete') 

1217 

1218 permissions = ('logs:DeleteDestination',) 

1219 

1220 def process(self, resources): 

1221 client = local_session(self.manager.session_factory).client('logs') 

1222 for r in resources: 

1223 self.manager.retry( 

1224 client.delete_destination, 

1225 ignore_err_codes=('ResourceNotFoundException',), 

1226 destinationName=r['destinationName'], 

1227 ) 

1228 

1229 

1230@resources.register("delivery-destination") 

1231class DeliveryDestination(QueryResourceManager): 

1232 class resource_type(TypeInfo): 

1233 service = "logs" 

1234 enum_spec = ('describe_delivery_destinations', 'deliveryDestinations', None) 

1235 arn_type = "delivery-destination" 

1236 arn_separator = ":" 

1237 arn = "arn" 

1238 id = name = "name" 

1239 cfn_type = "AWS::Logs::DeliveryDestination" 

1240 universal_taggable = object() 

1241 

1242 retry = staticmethod(get_retry( 

1243 ('ConflictException', 'ServiceUnavailableException', 'ThrottlingException',) 

1244 )) 

1245 source_mapping = { 

1246 "describe": DescribeWithResourceTags, 

1247 } 

1248 

1249 

1250@DeliveryDestination.filter_registry.register('cross-account') 

1251class DeliveryDestinationCrossAccount(CrossAccountAccessFilter): 

1252 

1253 policy_attribute = 'c7n:Policy' 

1254 permissions = ('logs:GetDeliveryDestinationPolicy',) 

1255 

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

1257 client = local_session(self.manager.session_factory).client('logs') 

1258 

1259 for r in resources: 

1260 resp = self.manager.retry( 

1261 client.get_delivery_destination_policy, 

1262 deliveryDestinationName=r['name'], 

1263 ignore_err_codes=('ResourceNotFoundException',) 

1264 ) 

1265 r[self.policy_attribute] = resp['policy']['deliveryDestinationPolicy'] 

1266 return super().process(resources) 

1267 

1268 

1269@DeliveryDestination.action_registry.register('delete') 

1270class DeliveryDestinationDelete(BaseAction): 

1271 """Action to delete a delivery destination 

1272 

1273 :example: 

1274 

1275 .. code-block:: yaml 

1276 

1277 policies: 

1278 - name: delete-delivery-destination 

1279 resource: aws.delivery-destination 

1280 filters: 

1281 - type: value 

1282 key: deliveryDestinationType 

1283 value: S3 

1284 actions: 

1285 - delete 

1286 """ 

1287 schema = type_schema('delete') 

1288 

1289 permissions = ('logs:DeleteDeliveryDestination',) 

1290 

1291 def process(self, resources): 

1292 client = local_session(self.manager.session_factory).client('logs') 

1293 for r in resources: 

1294 self.manager.retry( 

1295 client.delete_delivery_destination, 

1296 ignore_err_codes=('ResourceNotFoundException',), 

1297 name=r['name'], 

1298 )