Source code for c7n.resources.cw

# Copyright 2016-2017 Capital One Services, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import absolute_import, division, print_function, unicode_literals

from concurrent.futures import as_completed
from datetime import datetime, timedelta

from c7n.actions import BaseAction
from c7n.exceptions import PolicyValidationError
from c7n.filters import Filter, MetricsFilter
from c7n.filters.iamaccess import CrossAccountAccessFilter
from c7n.query import QueryResourceManager, ChildResourceManager
from c7n.manager import resources
from c7n.resolver import ValuesFrom
from c7n.tags import universal_augment, register_universal_tags
from c7n.utils import type_schema, local_session, chunks, get_retry


[docs]@resources.register('alarm') class Alarm(QueryResourceManager):
[docs] class resource_type(object): service = 'cloudwatch' type = 'alarm' enum_spec = ('describe_alarms', 'MetricAlarms', None) id = 'AlarmArn' filter_name = 'AlarmNames' filter_type = 'list' name = 'AlarmName' date = 'AlarmConfigurationUpdatedTimestamp' dimension = None config_type = 'AWS::CloudWatch::Alarm'
retry = staticmethod(get_retry(('Throttled',)))
[docs]@Alarm.action_registry.register('delete') class AlarmDelete(BaseAction): """Delete a cloudwatch alarm. :example: .. code-block:: yaml policies: - name: cloudwatch-delete-stale-alarms resource: alarm filters: - type: value value_type: age key: StateUpdatedTimestamp value: 30 op: ge - StateValue: INSUFFICIENT_DATA actions: - delete """ schema = type_schema('delete') permissions = ('cloudwatch:DeleteAlarms',)
[docs] def process(self, resources): client = local_session( self.manager.session_factory).client('cloudwatch') for resource_set in chunks(resources, size=100): self.manager.retry( client.delete_alarms, AlarmNames=[r['AlarmName'] for r in resource_set])
[docs]@resources.register('event-rule') class EventRule(QueryResourceManager):
[docs] class resource_type(object): service = 'events' type = 'event-rule' enum_spec = ('list_rules', 'Rules', None) name = "Name" id = "Name" filter_name = "NamePrefix" filter_type = "scalar" dimension = None
[docs]@EventRule.filter_registry.register('metrics') class EventRuleMetrics(MetricsFilter):
[docs] def get_dimensions(self, resource): return [{'Name': 'RuleName', 'Value': resource['Name']}]
[docs]@resources.register('event-rule-target') class EventRuleTarget(ChildResourceManager):
[docs] class resource_type(object): service = 'events' type = 'event-rule-target' enum_spec = ('list_targets_by_rule', 'Targets', None) parent_spec = ('event-rule', 'Rule', True) name = id = 'Id' dimension = None filter_type = filter_name = None
[docs]@EventRuleTarget.filter_registry.register('cross-account') class CrossAccountFilter(CrossAccountAccessFilter): schema = type_schema( 'cross-account', # white list accounts whitelist_from=ValuesFrom.schema, whitelist={'type': 'array', 'items': {'type': 'string'}}) # dummy permission permissions = ('events:ListTargetsByRule',) def __call__(self, r): account_id = r['Arn'].split(':', 5)[4] return account_id not in self.accounts
[docs]@EventRuleTarget.action_registry.register('delete') class DeleteTarget(BaseAction): schema = type_schema('delete') permissions = ('events:RemoveTargets',)
[docs] def process(self, resources): client = local_session(self.manager.session_factory).client('events') rule_targets = {} for r in resources: rule_targets.setdefault(r['c7n:parent-id'], []).append(r['Id']) for rule_id, target_ids in rule_targets.items(): client.remove_targets( Ids=target_ids, Rule=rule_id)
[docs]@resources.register('log-group') class LogGroup(QueryResourceManager):
[docs] class resource_type(object): service = 'logs' type = 'log-group' enum_spec = ('describe_log_groups', 'logGroups', None) name = 'logGroupName' id = 'arn' filter_name = 'logGroupNamePrefix' filter_type = 'scalar' dimension = 'LogGroupName' date = 'creationTime'
augment = universal_augment
[docs] def get_arns(self, resources): # log group arn in resource describe has ':*' suffix, not all # apis can use that form, so normalize to standard arn. return [r['arn'][:-2] for r in resources]
register_universal_tags(LogGroup.filter_registry, LogGroup.action_registry)
[docs]@LogGroup.action_registry.register('retention') class Retention(BaseAction): """Action to set the retention period (in days) for CloudWatch log groups :example: .. code-block:: yaml policies: - name: cloudwatch-set-log-group-retention resource: log-group actions: - type: retention days: 200 """ schema = type_schema('retention', days={'type': 'integer'}) permissions = ('logs:PutRetentionPolicy',)
[docs] def process(self, resources): client = local_session(self.manager.session_factory).client('logs') days = self.data['days'] for r in resources: client.put_retention_policy( logGroupName=r['logGroupName'], retentionInDays=days)
[docs]@LogGroup.action_registry.register('delete') class Delete(BaseAction): """ :example: .. code-block:: yaml policies: - name: cloudwatch-delete-stale-log-group resource: log-group filters: - type: last-write days: 182.5 actions: - delete """ schema = type_schema('delete') permissions = ('logs:DeleteLogGroup',)
[docs] def process(self, resources): client = local_session(self.manager.session_factory).client('logs') for r in resources: client.delete_log_group(logGroupName=r['logGroupName'])
[docs]@LogGroup.filter_registry.register('last-write') class LastWriteDays(Filter): """Filters CloudWatch log groups by last write :example: .. code-block:: yaml policies: - name: cloudwatch-stale-groups resource: log-group filters: - type: last-write days: 60 """ schema = type_schema( 'last-write', days={'type': 'number'}) permissions = ('logs:DescribeLogStreams',)
[docs] def process(self, resources, event=None): client = local_session(self.manager.session_factory).client('logs') self.date_threshold = datetime.utcnow() - timedelta( days=self.data['days']) return [r for r in resources if self.check_group(client, r)]
[docs] def check_group(self, client, group): streams = client.describe_log_streams( logGroupName=group['logGroupName'], orderBy='LastEventTime', descending=True, limit=3).get('logStreams') group['streams'] = streams if not streams: last_timestamp = group['creationTime'] elif streams[0]['storedBytes'] == 0: last_timestamp = streams[0]['creationTime'] else: last_timestamp = streams[0]['lastIngestionTime'] last_write = datetime.fromtimestamp(last_timestamp / 1000.0) group['lastWrite'] = last_write return self.date_threshold > last_write
[docs]@LogGroup.filter_registry.register('cross-account') class LogCrossAccountFilter(CrossAccountAccessFilter): schema = type_schema( 'cross-account', # white list accounts whitelist_from=ValuesFrom.schema, whitelist={'type': 'array', 'items': {'type': 'string'}}) permissions = ('logs:DescribeSubscriptionFilters',)
[docs] def process(self, resources, event=None): client = local_session(self.manager.session_factory).client('logs') accounts = self.get_accounts() results = [] with self.executor_factory(max_workers=1) as w: futures = [] for rset in chunks(resources, 50): futures.append( w.submit( self.process_resource_set, client, accounts, rset)) for f in as_completed(futures): if f.exception(): self.log.error( "Error checking log groups cross-account %s", f.exception()) continue results.extend(f.result()) return results
[docs] def process_resource_set(self, client, accounts, resources): results = [] for r in resources: found = False filters = self.manager.retry( client.describe_subscription_filters, logGroupName=r['logGroupName']).get('subscriptionFilters', ()) for f in filters: if 'destinationArn' not in f: continue account_id = f['destinationArn'].split(':', 5)[4] if account_id not in accounts: r.setdefault('c7n:CrossAccountViolations', []).append( account_id) found = True if found: results.append(r) return results
[docs]@LogGroup.action_registry.register('set-encryption') class EncryptLogGroup(BaseAction): """Encrypt/Decrypt a log group :example: .. code-block: yaml policies: - name: encrypt-log-group resource: log-group filters: - kmsKeyId: absent actions: - type: set-encryption kms-key: alias/mylogkey state: True - name: decrypt-log-group resource: log-group filters: - kmsKeyId: kms:key:arn actions: - type: set-encryption state: False """ schema = type_schema( 'set-encryption', **{'kms-key': {'type': 'string'}, 'state': {'type': 'boolean'}}) permissions = ( 'logs:AssociateKmsKey', 'logs:DisassociateKmsKey', 'kms:DescribeKey')
[docs] def validate(self): if not self.data.get('state', True): return self key = self.data.get('kms-key', '') if not key: raise ValueError('Must specify either a KMS key ARN or Alias') if 'alias/' not in key and ':key/' not in key: raise PolicyValidationError( "Invalid kms key format %s" % key) return self
[docs] def resolve_key(self, key): if not key: return # Qualified arn for key if key.startswith('arn:') and ':key/' in key: return key # Alias key = local_session( self.manager.session_factory).client( 'kms').describe_key( KeyId=key)['KeyMetadata']['Arn'] return key
[docs] def process(self, resources): session = local_session(self.manager.session_factory) client = session.client('logs') state = self.data.get('state', True) key = self.resolve_key(self.data.get('kms-key')) for r in resources: try: if state: client.associate_kms_key( logGroupName=r['logGroupName'], kmsKeyId=key) else: client.disassociate_kms_key(logGroupName=r['logGroupName']) except client.exceptions.ResourceNotFoundException: continue