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 )