Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/c7n/resources/account.py: 30%
994 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
« 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
3"""AWS Account as a custodian resource.
4"""
5import json
6import time
7import datetime
8from contextlib import suppress
9from botocore.exceptions import ClientError
10from fnmatch import fnmatch
11from dateutil.parser import parse as parse_date
12from dateutil.tz import tzutc
14from c7n.actions import ActionRegistry, BaseAction
15from c7n.exceptions import PolicyValidationError
16from c7n.filters import Filter, FilterRegistry, ValueFilter
17from c7n.filters.kms import KmsRelatedFilter
18from c7n.filters.multiattr import MultiAttrFilter
19from c7n.filters.missing import Missing
20from c7n.manager import ResourceManager, resources
21from c7n.utils import local_session, type_schema, generate_arn, get_support_region, jmespath_search
22from c7n.query import QueryResourceManager, TypeInfo
24from c7n.resources.iam import CredentialReport
25from c7n.resources.securityhub import OtherResourcePostFinding
27from .aws import shape_validate, Arn
29filters = FilterRegistry('aws.account.filters')
30actions = ActionRegistry('aws.account.actions')
32retry = staticmethod(QueryResourceManager.retry)
33filters.register('missing', Missing)
36def get_account(session_factory, config):
37 session = local_session(session_factory)
38 client = session.client('iam')
39 aliases = client.list_account_aliases().get(
40 'AccountAliases', ('',))
41 name = aliases and aliases[0] or ""
42 return {'account_id': config.account_id,
43 'account_name': name}
46@resources.register('account')
47class Account(ResourceManager):
49 filter_registry = filters
50 action_registry = actions
51 retry = staticmethod(QueryResourceManager.retry)
52 source_type = 'describe'
54 class resource_type(TypeInfo):
55 id = 'account_id'
56 name = 'account_name'
57 filter_name = None
58 global_resource = True
59 # fake this for doc gen
60 service = "account"
61 # for posting config rule evaluations
62 cfn_type = 'AWS::::Account'
64 @classmethod
65 def get_permissions(cls):
66 return ('iam:ListAccountAliases',)
68 @classmethod
69 def has_arn(cls):
70 return True
72 def get_arns(self, resources):
73 return ["arn:::{account_id}".format(**r) for r in resources]
75 def get_model(self):
76 return self.resource_type
78 def resources(self):
79 return self.filter_resources([get_account(self.session_factory, self.config)])
81 def get_resources(self, resource_ids):
82 return [get_account(self.session_factory, self.config)]
85@filters.register('credential')
86class AccountCredentialReport(CredentialReport):
88 def process(self, resources, event=None):
89 super(AccountCredentialReport, self).process(resources, event)
90 report = self.get_credential_report()
91 if report is None:
92 return []
93 results = []
94 info = report.get('<root_account>')
95 for r in resources:
96 if self.match(r, info):
97 r['c7n:credential-report'] = info
98 results.append(r)
99 return results
102@filters.register('organization')
103class AccountOrganization(ValueFilter):
104 """Check organization enrollment and configuration
106 :example:
108 determine if an account is not in an organization
110 .. code-block:: yaml
112 policies:
113 - name: no-org
114 resource: account
115 filters:
116 - type: organization
117 key: Id
118 value: absent
121 :example:
123 determine if an account is setup for organization policies
125 .. code-block:: yaml
127 policies:
128 - name: org-policies-not-enabled
129 resource: account
130 filters:
131 - type: organization
132 key: FeatureSet
133 value: ALL
134 op: not-equal
135 """
136 schema = type_schema('organization', rinherit=ValueFilter.schema)
137 schema_alias = False
139 annotation_key = 'c7n:org'
140 annotate = False
142 permissions = ('organizations:DescribeOrganization',)
144 def get_org_info(self, account):
145 client = local_session(
146 self.manager.session_factory).client('organizations')
147 try:
148 org_info = client.describe_organization().get('Organization')
149 except client.exceptions.AWSOrganizationsNotInUseException:
150 org_info = {}
151 except ClientError as e:
152 self.log.warning('organization filter error accessing org info %s', e)
153 org_info = None
154 account[self.annotation_key] = org_info
156 def process(self, resources, event=None):
157 if self.annotation_key not in resources[0]:
158 self.get_org_info(resources[0])
159 # if we can't access org info, we've already logged, and return
160 if resources[0][self.annotation_key] is None:
161 return []
162 if super().process([resources[0][self.annotation_key]]):
163 return resources
164 return []
167@filters.register('check-macie')
168class MacieEnabled(ValueFilter):
169 """Check status of macie v2 in the account.
171 Gets the macie session info for the account, and
172 the macie master account for the current account if
173 configured.
174 """
176 schema = type_schema('check-macie', rinherit=ValueFilter.schema)
177 schema_alias = False
178 annotation_key = 'c7n:macie'
179 annotate = False
180 permissions = ('macie2:GetMacieSession', 'macie2:GetMasterAccount',)
182 def process(self, resources, event=None):
184 if self.annotation_key not in resources[0]:
185 self.get_macie_info(resources[0])
187 if super().process([resources[0][self.annotation_key]]):
188 return resources
190 return []
192 def get_macie_info(self, account):
193 client = local_session(
194 self.manager.session_factory).client('macie2')
196 try:
197 info = client.get_macie_session()
198 info.pop('ResponseMetadata')
199 except client.exceptions.AccessDeniedException:
200 info = {}
202 try:
203 minfo = client.get_master_account().get('master')
204 except (client.exceptions.AccessDeniedException,
205 client.exceptions.ResourceNotFoundException):
206 info['master'] = {}
207 else:
208 info['master'] = minfo
209 account[self.annotation_key] = info
212@filters.register('check-cloudtrail')
213class CloudTrailEnabled(Filter):
214 """Verify cloud trail enabled for this account per specifications.
216 Returns an annotated account resource if trail is not enabled.
218 Of particular note, the current-region option will evaluate whether cloudtrail is available
219 in the current region, either as a multi region trail or as a trail with it as the home region.
221 The log-metric-filter-pattern option checks for the existence of a cloudwatch alarm and a
222 corresponding SNS subscription for a specific filter pattern
224 :example:
226 .. code-block:: yaml
228 policies:
229 - name: account-cloudtrail-enabled
230 resource: account
231 region: us-east-1
232 filters:
233 - type: check-cloudtrail
234 global-events: true
235 multi-region: true
236 running: true
237 include-management-events: true
238 log-metric-filter-pattern: "{ ($.eventName = \\"ConsoleLogin\\") }"
240 Check for CloudWatch log group with a metric filter that has a filter pattern
241 matching a regex pattern:
243 .. code-block:: yaml
245 policies:
246 - name: account-cloudtrail-with-matching-log-metric-filter
247 resource: account
248 region: us-east-1
249 filters:
250 - type: check-cloudtrail
251 log-metric-filter-pattern:
252 type: value
253 op: regex
254 value: '\\{ ?(\\()? ?\\$\\.eventName ?= ?(")?ConsoleLogin(")? ?(\\))? ?\\}'
255 """
256 schema = type_schema(
257 'check-cloudtrail',
258 **{'multi-region': {'type': 'boolean'},
259 'global-events': {'type': 'boolean'},
260 'current-region': {'type': 'boolean'},
261 'running': {'type': 'boolean'},
262 'notifies': {'type': 'boolean'},
263 'file-digest': {'type': 'boolean'},
264 'kms': {'type': 'boolean'},
265 'kms-key': {'type': 'string'},
266 'include-management-events': {'type': 'boolean'},
267 'log-metric-filter-pattern': {'oneOf': [
268 {'$ref': '#/definitions/filters/value'},
269 {'type': 'string'}]}})
271 permissions = ('cloudtrail:DescribeTrails', 'cloudtrail:GetTrailStatus',
272 'cloudtrail:GetEventSelectors', 'cloudwatch:DescribeAlarmsForMetric',
273 'logs:DescribeMetricFilters', 'sns:GetTopicAttributes')
275 def process(self, resources, event=None):
276 session = local_session(self.manager.session_factory)
277 client = session.client('cloudtrail')
278 trails = client.describe_trails()['trailList']
279 resources[0]['c7n:cloudtrails'] = trails
281 if self.data.get('global-events'):
282 trails = [t for t in trails if t.get('IncludeGlobalServiceEvents')]
283 if self.data.get('current-region'):
284 current_region = session.region_name
285 trails = [t for t in trails if t.get(
286 'HomeRegion') == current_region or t.get('IsMultiRegionTrail')]
287 if self.data.get('kms'):
288 trails = [t for t in trails if t.get('KmsKeyId')]
289 if self.data.get('kms-key'):
290 trails = [t for t in trails
291 if t.get('KmsKeyId', '') == self.data['kms-key']]
292 if self.data.get('file-digest'):
293 trails = [t for t in trails
294 if t.get('LogFileValidationEnabled')]
295 if self.data.get('multi-region'):
296 trails = [t for t in trails if t.get('IsMultiRegionTrail')]
297 if self.data.get('notifies'):
298 trails = [t for t in trails if t.get('SnsTopicARN')]
299 if self.data.get('running', True):
300 running = []
301 for t in list(trails):
302 t['Status'] = status = client.get_trail_status(
303 Name=t['TrailARN'])
304 if status['IsLogging'] and not status.get(
305 'LatestDeliveryError'):
306 running.append(t)
307 trails = running
308 if self.data.get('include-management-events'):
309 matched = []
310 for t in list(trails):
311 selectors = client.get_event_selectors(TrailName=t['TrailARN'])
312 if 'EventSelectors' in selectors.keys():
313 for s in selectors['EventSelectors']:
314 if s['IncludeManagementEvents'] and s['ReadWriteType'] == 'All':
315 matched.append(t)
316 elif 'AdvancedEventSelectors' in selectors.keys():
317 for s in selectors['AdvancedEventSelectors']:
318 management = False
319 readonly = False
320 for field_selector in s['FieldSelectors']:
321 if field_selector['Field'] == 'eventCategory' and \
322 'Management' in field_selector['Equals']:
323 management = True
324 elif field_selector['Field'] == 'readOnly':
325 readonly = True
326 if management and not readonly:
327 matched.append(t)
329 trails = matched
330 if self.data.get('log-metric-filter-pattern'):
331 client_logs = session.client('logs')
332 client_cw = session.client('cloudwatch')
333 client_sns = session.client('sns')
334 matched = []
335 pattern = self.data.get('log-metric-filter-pattern')
336 if isinstance(pattern, str):
337 vf = ValueFilter({'key': 'filterPattern', 'value': pattern})
338 else:
339 pattern.setdefault('key', 'filterPattern')
340 vf = ValueFilter(pattern)
342 for t in list(trails):
343 if 'CloudWatchLogsLogGroupArn' not in t.keys():
344 continue
345 log_group_name = t['CloudWatchLogsLogGroupArn'].split(':')[6]
346 try:
347 metric_filters_log_group = \
348 client_logs.describe_metric_filters(
349 logGroupName=log_group_name)['metricFilters']
350 except ClientError as e:
351 if e.response['Error']['Code'] == 'ResourceNotFoundException':
352 continue
353 filter_matched = None
354 if metric_filters_log_group:
355 for f in metric_filters_log_group:
356 if vf(f):
357 filter_matched = f
358 break
359 if not filter_matched:
360 continue
361 alarms = client_cw.describe_alarms_for_metric(
362 MetricName=filter_matched["metricTransformations"][0]["metricName"],
363 Namespace=filter_matched["metricTransformations"][0]["metricNamespace"]
364 )['MetricAlarms']
365 alarm_actions = []
366 for a in alarms:
367 alarm_actions.extend(a['AlarmActions'])
368 if not alarm_actions:
369 continue
370 alarm_actions = set(alarm_actions)
371 for a in alarm_actions:
372 try:
373 sns_topic_attributes = client_sns.get_topic_attributes(TopicArn=a)
374 sns_topic_attributes = sns_topic_attributes.get('Attributes')
375 if sns_topic_attributes.get('SubscriptionsConfirmed', '0') != '0':
376 matched.append(t)
377 except client_sns.exceptions.InvalidParameterValueException:
378 # we can ignore any exception here, the alarm action might
379 # not be an sns topic for instance
380 continue
381 trails = matched
382 if trails:
383 return []
384 return resources
387@filters.register('guard-duty')
388class GuardDutyEnabled(MultiAttrFilter):
389 """Check if the guard duty service is enabled.
391 This allows looking at account's detector and its associated
392 master if any.
394 :example:
396 Check to ensure guard duty is active on account and associated to a master.
398 .. code-block:: yaml
400 policies:
401 - name: guardduty-enabled
402 resource: account
403 filters:
404 - type: guard-duty
405 Detector.Status: ENABLED
406 Master.AccountId: "00011001"
407 Master.RelationshipStatus: "Enabled"
408 """
410 schema = {
411 'type': 'object',
412 'additionalProperties': False,
413 'properties': {
414 'type': {'enum': ['guard-duty']},
415 'match-operator': {'enum': ['or', 'and']}},
416 'patternProperties': {
417 '^Detector': {'oneOf': [{'type': 'object'}, {'type': 'string'}]},
418 '^Master': {'oneOf': [{'type': 'object'}, {'type': 'string'}]}},
419 }
421 annotation = "c7n:guard-duty"
422 permissions = (
423 'guardduty:GetMasterAccount',
424 'guardduty:ListDetectors',
425 'guardduty:GetDetector')
427 def validate(self):
428 attrs = set()
429 for k in self.data:
430 if k.startswith('Detector') or k.startswith('Master'):
431 attrs.add(k)
432 self.multi_attrs = attrs
433 return super(GuardDutyEnabled, self).validate()
435 def get_target(self, resource):
436 if self.annotation in resource:
437 return resource[self.annotation]
439 client = local_session(self.manager.session_factory).client('guardduty')
440 # detectors are singletons too.
441 detector_ids = client.list_detectors().get('DetectorIds')
443 if not detector_ids:
444 return None
445 else:
446 detector_id = detector_ids.pop()
448 detector = client.get_detector(DetectorId=detector_id)
449 detector.pop('ResponseMetadata', None)
450 master = client.get_master_account(DetectorId=detector_id).get('Master')
451 resource[self.annotation] = r = {'Detector': detector, 'Master': master}
452 return r
455@filters.register('check-config')
456class ConfigEnabled(Filter):
457 """Is config service enabled for this account
459 :example:
461 .. code-block:: yaml
463 policies:
464 - name: account-check-config-services
465 resource: account
466 region: us-east-1
467 filters:
468 - type: check-config
469 all-resources: true
470 global-resources: true
471 running: true
472 """
474 schema = type_schema(
475 'check-config', **{
476 'all-resources': {'type': 'boolean'},
477 'running': {'type': 'boolean'},
478 'global-resources': {'type': 'boolean'}})
480 permissions = ('config:DescribeDeliveryChannels',
481 'config:DescribeConfigurationRecorders',
482 'config:DescribeConfigurationRecorderStatus')
484 def process(self, resources, event=None):
485 client = local_session(
486 self.manager.session_factory).client('config')
487 channels = client.describe_delivery_channels()[
488 'DeliveryChannels']
489 recorders = client.describe_configuration_recorders()[
490 'ConfigurationRecorders']
491 resources[0]['c7n:config_recorders'] = recorders
492 resources[0]['c7n:config_channels'] = channels
493 if self.data.get('global-resources'):
494 recorders = [
495 r for r in recorders
496 if r['recordingGroup'].get('includeGlobalResourceTypes')]
497 if self.data.get('all-resources'):
498 recorders = [r for r in recorders
499 if r['recordingGroup'].get('allSupported')]
500 if self.data.get('running', True) and recorders:
501 status = {s['name']: s for
502 s in client.describe_configuration_recorder_status(
503 )['ConfigurationRecordersStatus']}
504 resources[0]['c7n:config_status'] = status
505 recorders = [r for r in recorders if status[r['name']]['recording'] and
506 status[r['name']]['lastStatus'].lower() in ('pending', 'success')]
507 if channels and recorders:
508 return []
509 return resources
512@filters.register('iam-summary')
513class IAMSummary(ValueFilter):
514 """Return annotated account resource if iam summary filter matches.
516 Some use cases include, detecting root api keys or mfa usage.
518 Example iam summary wrt to matchable fields::
520 {
521 "AccessKeysPerUserQuota": 2,
522 "AccountAccessKeysPresent": 0,
523 "AccountMFAEnabled": 1,
524 "AccountSigningCertificatesPresent": 0,
525 "AssumeRolePolicySizeQuota": 2048,
526 "AttachedPoliciesPerGroupQuota": 10,
527 "AttachedPoliciesPerRoleQuota": 10,
528 "AttachedPoliciesPerUserQuota": 10,
529 "GroupPolicySizeQuota": 5120,
530 "Groups": 1,
531 "GroupsPerUserQuota": 10,
532 "GroupsQuota": 100,
533 "InstanceProfiles": 0,
534 "InstanceProfilesQuota": 100,
535 "MFADevices": 3,
536 "MFADevicesInUse": 2,
537 "Policies": 3,
538 "PoliciesQuota": 1000,
539 "PolicySizeQuota": 5120,
540 "PolicyVersionsInUse": 5,
541 "PolicyVersionsInUseQuota": 10000,
542 "Providers": 0,
543 "RolePolicySizeQuota": 10240,
544 "Roles": 4,
545 "RolesQuota": 250,
546 "ServerCertificates": 0,
547 "ServerCertificatesQuota": 20,
548 "SigningCertificatesPerUserQuota": 2,
549 "UserPolicySizeQuota": 2048,
550 "Users": 5,
551 "UsersQuota": 5000,
552 "VersionsPerPolicyQuota": 5,
553 }
555 For example to determine if an account has either not been
556 enabled with root mfa or has root api keys.
558 .. code-block:: yaml
560 policies:
561 - name: root-keys-or-no-mfa
562 resource: account
563 filters:
564 - type: iam-summary
565 key: AccountMFAEnabled
566 value: true
567 op: eq
568 value_type: swap
569 """
570 schema = type_schema('iam-summary', rinherit=ValueFilter.schema)
571 schema_alias = False
572 permissions = ('iam:GetAccountSummary',)
574 def process(self, resources, event=None):
575 if not resources[0].get('c7n:iam_summary'):
576 client = local_session(
577 self.manager.session_factory).client('iam')
578 resources[0]['c7n:iam_summary'] = client.get_account_summary(
579 )['SummaryMap']
580 if self.match(resources[0]['c7n:iam_summary']):
581 return resources
582 return []
585@filters.register('access-analyzer')
586class AccessAnalyzer(ValueFilter):
587 """Check for access analyzers in an account
589 :example:
591 .. code-block:: yaml
593 policies:
594 - name: account-access-analyzer
595 resource: account
596 filters:
597 - type: access-analyzer
598 key: 'status'
599 value: ACTIVE
600 op: eq
601 """
603 schema = type_schema('access-analyzer', rinherit=ValueFilter.schema)
604 schema_alias = False
605 permissions = ('access-analyzer:ListAnalyzers',)
606 annotation_key = 'c7n:matched-analyzers'
608 def process(self, resources, event=None):
609 account = resources[0]
610 if not account.get(self.annotation_key):
611 client = local_session(self.manager.session_factory).client('accessanalyzer')
612 analyzers = self.manager.retry(client.list_analyzers)['analyzers']
613 else:
614 analyzers = account.get(self.annotation_key)
616 matched_analyzers = []
617 for analyzer in analyzers:
618 if self.match(analyzer):
619 matched_analyzers.append(analyzer)
620 account[self.annotation_key] = matched_analyzers
621 return matched_analyzers and resources or []
624@filters.register('password-policy')
625class AccountPasswordPolicy(ValueFilter):
626 """Check an account's password policy.
628 Note that on top of the default password policy fields, we also add an extra key,
629 PasswordPolicyConfigured which will be set to true or false to signify if the given
630 account has attempted to set a policy at all.
632 :example:
634 .. code-block:: yaml
636 policies:
637 - name: password-policy-check
638 resource: account
639 region: us-east-1
640 filters:
641 - type: password-policy
642 key: MinimumPasswordLength
643 value: 10
644 op: ge
645 - type: password-policy
646 key: RequireSymbols
647 value: true
648 """
649 schema = type_schema('password-policy', rinherit=ValueFilter.schema)
650 schema_alias = False
651 permissions = ('iam:GetAccountPasswordPolicy',)
653 def process(self, resources, event=None):
654 account = resources[0]
655 if not account.get('c7n:password_policy'):
656 client = local_session(self.manager.session_factory).client('iam')
657 policy = {}
658 try:
659 policy = client.get_account_password_policy().get('PasswordPolicy', {})
660 policy['PasswordPolicyConfigured'] = True
661 except ClientError as e:
662 if e.response['Error']['Code'] == 'NoSuchEntity':
663 policy['PasswordPolicyConfigured'] = False
664 else:
665 raise
666 account['c7n:password_policy'] = policy
667 if self.match(account['c7n:password_policy']):
668 return resources
669 return []
672@actions.register('set-password-policy')
673class SetAccountPasswordPolicy(BaseAction):
674 """Set an account's password policy.
676 This only changes the policy for the items provided.
677 If this is the first time setting a password policy and an item is not provided it will be
678 set to the defaults defined in the boto docs for IAM.Client.update_account_password_policy
680 :example:
682 .. code-block:: yaml
684 policies:
685 - name: set-account-password-policy
686 resource: account
687 filters:
688 - not:
689 - type: password-policy
690 key: MinimumPasswordLength
691 value: 10
692 op: ge
693 actions:
694 - type: set-password-policy
695 policy:
696 MinimumPasswordLength: 20
697 """
698 schema = type_schema(
699 'set-password-policy',
700 policy={
701 'type': 'object'
702 })
703 shape = 'UpdateAccountPasswordPolicyRequest'
704 service = 'iam'
705 permissions = ('iam:GetAccountPasswordPolicy', 'iam:UpdateAccountPasswordPolicy')
707 def validate(self):
708 return shape_validate(
709 self.data.get('policy', {}),
710 self.shape,
711 self.service)
713 def process(self, resources):
714 client = local_session(self.manager.session_factory).client('iam')
715 account = resources[0]
716 if account.get('c7n:password_policy'):
717 config = account['c7n:password_policy']
718 else:
719 try:
720 config = client.get_account_password_policy().get('PasswordPolicy')
721 except client.exceptions.NoSuchEntityException:
722 config = {}
723 params = dict(self.data['policy'])
724 config.update(params)
725 config = {k: v for (k, v) in config.items() if k not in ('ExpirePasswords',
726 'PasswordPolicyConfigured')}
727 client.update_account_password_policy(**config)
730@filters.register('service-limit')
731class ServiceLimit(Filter):
732 """Check if account's service limits are past a given threshold.
734 Supported limits are per trusted advisor, which is variable based
735 on usage in the account and support level enabled on the account.
737 The `names` attribute lets you filter which checks to query limits
738 about. This is a case-insensitive globbing match on a check name.
739 You can specify a name exactly or use globbing wildcards like `VPC*`.
741 The names are exactly what's shown on the trusted advisor page:
743 https://console.aws.amazon.com/trustedadvisor/home#/category/service-limits
745 or via the awscli:
747 aws --region us-east-1 support describe-trusted-advisor-checks --language en \
748 --query 'checks[?category==`service_limits`].[name]' --output text
750 While you can target individual checks via the `names` attribute, and
751 that should be the preferred method, the following are provided for
752 backward compatibility with the old style of checks:
754 - `services`
756 The resulting limit's `service` field must match one of these.
757 These are case-insensitive globbing matches.
759 Note: If you haven't specified any `names` to filter, then
760 these service names are used as a case-insensitive prefix match on
761 the check name. This helps limit the number of API calls we need
762 to make.
764 - `limits`
766 The resulting limit's `Limit Name` field must match one of these.
767 These are case-insensitive globbing matches.
769 Some example names and their corresponding service and limit names:
771 Check Name Service Limit Name
772 ---------------------------------- -------------- ---------------------------------
773 Auto Scaling Groups AutoScaling Auto Scaling groups
774 Auto Scaling Launch Configurations AutoScaling Launch configurations
775 CloudFormation Stacks CloudFormation Stacks
776 ELB Application Load Balancers ELB Active Application Load Balancers
777 ELB Classic Load Balancers ELB Active load balancers
778 ELB Network Load Balancers ELB Active Network Load Balancers
779 VPC VPC VPCs
780 VPC Elastic IP Address VPC VPC Elastic IP addresses (EIPs)
781 VPC Internet Gateways VPC Internet gateways
783 Note: Some service limits checks are being migrated to service quotas,
784 which is expected to largely replace service limit checks in trusted
785 advisor. In this case, some of these checks have no results.
787 :example:
789 .. code-block:: yaml
791 policies:
792 - name: specific-account-service-limits
793 resource: account
794 filters:
795 - type: service-limit
796 names:
797 - IAM Policies
798 - IAM Roles
799 - "VPC*"
800 threshold: 1.0
802 - name: increase-account-service-limits
803 resource: account
804 filters:
805 - type: service-limit
806 services:
807 - EC2
808 threshold: 1.0
810 - name: specify-region-for-global-service
811 region: us-east-1
812 resource: account
813 filters:
814 - type: service-limit
815 services:
816 - IAM
817 limits:
818 - Roles
819 """
821 schema = type_schema(
822 'service-limit',
823 threshold={'type': 'number'},
824 refresh_period={'type': 'integer',
825 'title': 'how long should a check result be considered fresh'},
826 names={'type': 'array', 'items': {'type': 'string'}},
827 limits={'type': 'array', 'items': {'type': 'string'}},
828 services={'type': 'array', 'items': {
829 'enum': ['AutoScaling', 'CloudFormation',
830 'DynamoDB', 'EBS', 'EC2', 'ELB',
831 'IAM', 'RDS', 'Route53', 'SES', 'VPC']}})
833 permissions = ('support:DescribeTrustedAdvisorCheckRefreshStatuses',
834 'support:DescribeTrustedAdvisorCheckResult',
835 'support:DescribeTrustedAdvisorChecks',
836 'support:RefreshTrustedAdvisorCheck')
837 deprecated_check_ids = ['eW7HH0l7J9']
838 check_limit = ('region', 'service', 'check', 'limit', 'extant', 'color')
840 # When doing a refresh, how long to wait for the check to become ready.
841 # Max wait here is 5 * 10 ~ 50 seconds.
842 poll_interval = 5
843 poll_max_intervals = 10
844 global_services = {'IAM'}
846 def validate(self):
847 region = self.manager.data.get('region', '')
848 if len(self.global_services.intersection(self.data.get('services', []))):
849 if region != 'us-east-1':
850 raise PolicyValidationError(
851 "Global services: %s must be targeted in us-east-1 on the policy"
852 % ', '.join(self.global_services))
853 return self
855 @classmethod
856 def get_check_result(cls, client, check_id):
857 checks = client.describe_trusted_advisor_check_result(
858 checkId=check_id, language='en')['result']
860 # Check status and if necessary refresh checks
861 if checks['status'] == 'not_available':
862 try:
863 client.refresh_trusted_advisor_check(checkId=check_id)
864 except ClientError as e:
865 if e.response['Error']['Code'] == 'InvalidParameterValueException':
866 cls.log.warning("InvalidParameterValueException: %s",
867 e.response['Error']['Message'])
868 return
870 for _ in range(cls.poll_max_intervals):
871 time.sleep(cls.poll_interval)
872 refresh_response = client.describe_trusted_advisor_check_refresh_statuses(
873 checkIds=[check_id])
874 if refresh_response['statuses'][0]['status'] == 'success':
875 checks = client.describe_trusted_advisor_check_result(
876 checkId=check_id, language='en')['result']
877 break
878 return checks
880 def get_available_checks(self, client, category='service_limits'):
881 checks = client.describe_trusted_advisor_checks(language='en')
882 return [c for c in checks['checks']
883 if c['category'] == category and
884 c['id'] not in self.deprecated_check_ids]
886 def match_patterns_to_value(self, patterns, value):
887 for p in patterns:
888 if fnmatch(value.lower(), p.lower()):
889 return True
890 return False
892 def should_process(self, name):
893 # if names specified, limit to these names
894 patterns = self.data.get('names')
895 if patterns:
896 return self.match_patterns_to_value(patterns, name)
898 # otherwise, if services specified, limit to those prefixes
899 services = self.data.get('services')
900 if services:
901 patterns = ["{}*".format(i) for i in services]
902 return self.match_patterns_to_value(patterns, name.replace(' ', ''))
904 return True
906 def process(self, resources, event=None):
907 support_region = get_support_region(self.manager)
908 client = local_session(self.manager.session_factory).client(
909 'support', region_name=support_region)
910 checks = self.get_available_checks(client)
911 exceeded = []
912 for check in checks:
913 if not self.should_process(check['name']):
914 continue
915 matched = self.process_check(client, check, resources, event)
916 if matched:
917 for m in matched:
918 m['check_id'] = check['id']
919 m['name'] = check['name']
920 exceeded.extend(matched)
921 if exceeded:
922 resources[0]['c7n:ServiceLimitsExceeded'] = exceeded
923 return resources
924 return []
926 def process_check(self, client, check, resources, event=None):
927 region = self.manager.config.region
928 results = self.get_check_result(client, check['id'])
930 if results is None or 'flaggedResources' not in results:
931 return []
933 # trim to only results for this region
934 results['flaggedResources'] = [
935 r
936 for r in results.get('flaggedResources', [])
937 if r['metadata'][0] == region or (r['metadata'][0] == '-' and region == 'us-east-1')
938 ]
940 # save all raw limit results to the account resource
941 if 'c7n:ServiceLimits' not in resources[0]:
942 resources[0]['c7n:ServiceLimits'] = []
943 resources[0]['c7n:ServiceLimits'].append(results)
945 # check if we need to refresh the check for next time
946 delta = datetime.timedelta(self.data.get('refresh_period', 1))
947 check_date = parse_date(results['timestamp'])
948 if datetime.datetime.now(tz=tzutc()) - delta > check_date:
949 try:
950 client.refresh_trusted_advisor_check(checkId=check['id'])
951 except ClientError as e:
952 if e.response['Error']['Code'] == 'InvalidParameterValueException':
953 self.log.warning("InvalidParameterValueException: %s",
954 e.response['Error']['Message'])
955 return
957 services = self.data.get('services')
958 limits = self.data.get('limits')
959 threshold = self.data.get('threshold')
960 exceeded = []
962 for resource in results['flaggedResources']:
963 if threshold is None and resource['status'] == 'ok':
964 continue
965 limit = dict(zip(self.check_limit, resource['metadata']))
966 if services and not self.match_patterns_to_value(services, limit['service']):
967 continue
968 if limits and not self.match_patterns_to_value(limits, limit['check']):
969 continue
970 limit['status'] = resource['status']
971 limit['percentage'] = (
972 float(limit['extant'] or 0) / float(limit['limit']) * 100
973 )
974 if threshold and limit['percentage'] < threshold:
975 continue
976 exceeded.append(limit)
977 return exceeded
980@actions.register('request-limit-increase')
981class RequestLimitIncrease(BaseAction):
982 r"""File support ticket to raise limit.
984 :Example:
986 .. code-block:: yaml
988 policies:
989 - name: raise-account-service-limits
990 resource: account
991 filters:
992 - type: service-limit
993 services:
994 - EBS
995 limits:
996 - Provisioned IOPS (SSD) storage (GiB)
997 threshold: 60.5
998 actions:
999 - type: request-limit-increase
1000 notify: [email, email2]
1001 ## You can use one of either percent-increase or an amount-increase.
1002 percent-increase: 50
1003 message: "Please raise the below account limit(s); \n {limits}"
1004 """
1006 schema = {
1007 'type': 'object',
1008 'additionalProperties': False,
1009 'properties': {
1010 'type': {'enum': ['request-limit-increase']},
1011 'percent-increase': {'type': 'number', 'minimum': 1},
1012 'amount-increase': {'type': 'number', 'minimum': 1},
1013 'minimum-increase': {'type': 'number', 'minimum': 1},
1014 'subject': {'type': 'string'},
1015 'message': {'type': 'string'},
1016 'notify': {'type': 'array', 'items': {'type': 'string'}},
1017 'severity': {'type': 'string', 'enum': ['urgent', 'high', 'normal', 'low']}
1018 },
1019 'oneOf': [
1020 {'required': ['type', 'percent-increase']},
1021 {'required': ['type', 'amount-increase']}
1022 ]
1023 }
1025 permissions = ('support:CreateCase',)
1027 default_subject = '[Account:{account}]Raise the following limit(s) of {service} in {region}'
1028 default_template = 'Please raise the below account limit(s); \n {limits}'
1029 default_severity = 'normal'
1031 service_code_mapping = {
1032 'AutoScaling': 'auto-scaling',
1033 'CloudFormation': 'aws-cloudformation',
1034 'DynamoDB': 'amazon-dynamodb',
1035 'EBS': 'amazon-elastic-block-store',
1036 'EC2': 'amazon-elastic-compute-cloud-linux',
1037 'ELB': 'elastic-load-balancing',
1038 'IAM': 'aws-identity-and-access-management',
1039 'Kinesis': 'amazon-kinesis',
1040 'RDS': 'amazon-relational-database-service-aurora',
1041 'Route53': 'amazon-route53',
1042 'SES': 'amazon-simple-email-service',
1043 'VPC': 'amazon-virtual-private-cloud',
1044 }
1046 def process(self, resources):
1047 support_region = get_support_region(self.manager)
1048 client = local_session(self.manager.session_factory).client(
1049 'support', region_name=support_region)
1050 account_id = self.manager.config.account_id
1051 service_map = {}
1052 region_map = {}
1053 limit_exceeded = resources[0].get('c7n:ServiceLimitsExceeded', [])
1054 percent_increase = self.data.get('percent-increase')
1055 amount_increase = self.data.get('amount-increase')
1056 minimum_increase = self.data.get('minimum-increase', 1)
1058 for s in limit_exceeded:
1059 current_limit = int(s['limit'])
1060 if percent_increase:
1061 increase_by = current_limit * float(percent_increase) / 100
1062 increase_by = max(increase_by, minimum_increase)
1063 else:
1064 increase_by = amount_increase
1065 increase_by = round(increase_by)
1066 msg = '\nIncrease %s by %d in %s \n\t Current Limit: %s\n\t Current Usage: %s\n\t ' \
1067 'Set New Limit to: %d' % (
1068 s['check'], increase_by, s['region'], s['limit'], s['extant'],
1069 (current_limit + increase_by))
1070 service_map.setdefault(s['service'], []).append(msg)
1071 region_map.setdefault(s['service'], s['region'])
1073 for service in service_map:
1074 subject = self.data.get('subject', self.default_subject).format(
1075 service=service, region=region_map[service], account=account_id)
1076 service_code = self.service_code_mapping.get(service)
1077 body = self.data.get('message', self.default_template)
1078 body = body.format(**{
1079 'service': service,
1080 'limits': '\n\t'.join(service_map[service]),
1081 })
1082 client.create_case(
1083 subject=subject,
1084 communicationBody=body,
1085 serviceCode=service_code,
1086 categoryCode='general-guidance',
1087 severityCode=self.data.get('severity', self.default_severity),
1088 ccEmailAddresses=self.data.get('notify', []))
1091def cloudtrail_policy(original, bucket_name, account_id, bucket_region):
1092 '''add CloudTrail permissions to an S3 policy, preserving existing'''
1093 ct_actions = [
1094 {
1095 'Action': 's3:GetBucketAcl',
1096 'Effect': 'Allow',
1097 'Principal': {'Service': 'cloudtrail.amazonaws.com'},
1098 'Resource': generate_arn(
1099 service='s3', resource=bucket_name, region=bucket_region),
1100 'Sid': 'AWSCloudTrailAclCheck20150319',
1101 },
1102 {
1103 'Action': 's3:PutObject',
1104 'Condition': {
1105 'StringEquals':
1106 {'s3:x-amz-acl': 'bucket-owner-full-control'},
1107 },
1108 'Effect': 'Allow',
1109 'Principal': {'Service': 'cloudtrail.amazonaws.com'},
1110 'Resource': generate_arn(
1111 service='s3', resource=bucket_name, region=bucket_region),
1112 'Sid': 'AWSCloudTrailWrite20150319',
1113 },
1114 ]
1115 # parse original policy
1116 if original is None:
1117 policy = {
1118 'Statement': [],
1119 'Version': '2012-10-17',
1120 }
1121 else:
1122 policy = json.loads(original['Policy'])
1123 original_actions = [a.get('Action') for a in policy['Statement']]
1124 for cta in ct_actions:
1125 if cta['Action'] not in original_actions:
1126 policy['Statement'].append(cta)
1127 return json.dumps(policy)
1130# AWS Account doesn't participate in events (not based on query resource manager)
1131# so the event subscriber used by postfinding to register doesn't apply, manually
1132# register it.
1133Account.action_registry.register('post-finding', OtherResourcePostFinding)
1136@actions.register('enable-cloudtrail')
1137class EnableTrail(BaseAction):
1138 """Enables logging on the trail(s) named in the policy
1140 :Example:
1142 .. code-block:: yaml
1144 policies:
1145 - name: trail-test
1146 description: Ensure CloudTrail logging is enabled
1147 resource: account
1148 actions:
1149 - type: enable-cloudtrail
1150 trail: mytrail
1151 bucket: trails
1152 """
1154 permissions = (
1155 'cloudtrail:CreateTrail',
1156 'cloudtrail:DescribeTrails',
1157 'cloudtrail:GetTrailStatus',
1158 'cloudtrail:StartLogging',
1159 'cloudtrail:UpdateTrail',
1160 's3:CreateBucket',
1161 's3:GetBucketPolicy',
1162 's3:PutBucketPolicy',
1163 )
1164 schema = type_schema(
1165 'enable-cloudtrail',
1166 **{
1167 'trail': {'type': 'string'},
1168 'bucket': {'type': 'string'},
1169 'bucket-region': {'type': 'string'},
1170 'multi-region': {'type': 'boolean'},
1171 'global-events': {'type': 'boolean'},
1172 'notify': {'type': 'string'},
1173 'file-digest': {'type': 'boolean'},
1174 'kms': {'type': 'boolean'},
1175 'kms-key': {'type': 'string'},
1176 'required': ('bucket',),
1177 }
1178 )
1180 def process(self, accounts):
1181 """Create or enable CloudTrail"""
1182 session = local_session(self.manager.session_factory)
1183 client = session.client('cloudtrail')
1184 bucket_name = self.data['bucket']
1185 bucket_region = self.data.get('bucket-region', 'us-east-1')
1186 trail_name = self.data.get('trail', 'default-trail')
1187 multi_region = self.data.get('multi-region', True)
1188 global_events = self.data.get('global-events', True)
1189 notify = self.data.get('notify', '')
1190 file_digest = self.data.get('file-digest', False)
1191 kms = self.data.get('kms', False)
1192 kms_key = self.data.get('kms-key', '')
1194 s3client = session.client('s3', region_name=bucket_region)
1195 try:
1196 s3client.create_bucket(
1197 Bucket=bucket_name,
1198 CreateBucketConfiguration={'LocationConstraint': bucket_region}
1199 )
1200 except ClientError as ce:
1201 if not ('Error' in ce.response and
1202 ce.response['Error']['Code'] == 'BucketAlreadyOwnedByYou'):
1203 raise ce
1205 try:
1206 current_policy = s3client.get_bucket_policy(Bucket=bucket_name)
1207 except ClientError:
1208 current_policy = None
1210 policy_json = cloudtrail_policy(
1211 current_policy, bucket_name,
1212 self.manager.config.account_id, bucket_region)
1214 s3client.put_bucket_policy(Bucket=bucket_name, Policy=policy_json)
1215 trails = client.describe_trails().get('trailList', ())
1216 if trail_name not in [t.get('Name') for t in trails]:
1217 new_trail = client.create_trail(
1218 Name=trail_name,
1219 S3BucketName=bucket_name,
1220 )
1221 if new_trail:
1222 trails.append(new_trail)
1223 # the loop below will configure the new trail
1224 for trail in trails:
1225 if trail.get('Name') != trail_name:
1226 continue
1227 # enable
1228 arn = trail['TrailARN']
1229 status = client.get_trail_status(Name=arn)
1230 if not status['IsLogging']:
1231 client.start_logging(Name=arn)
1232 # apply configuration changes (if any)
1233 update_args = {}
1234 if multi_region != trail.get('IsMultiRegionTrail'):
1235 update_args['IsMultiRegionTrail'] = multi_region
1236 if global_events != trail.get('IncludeGlobalServiceEvents'):
1237 update_args['IncludeGlobalServiceEvents'] = global_events
1238 if notify != trail.get('SNSTopicArn'):
1239 update_args['SnsTopicName'] = notify
1240 if file_digest != trail.get('LogFileValidationEnabled'):
1241 update_args['EnableLogFileValidation'] = file_digest
1242 if kms_key != trail.get('KmsKeyId'):
1243 if not kms and 'KmsKeyId' in trail:
1244 kms_key = ''
1245 update_args['KmsKeyId'] = kms_key
1246 if update_args:
1247 update_args['Name'] = trail_name
1248 client.update_trail(**update_args)
1251@filters.register('has-virtual-mfa')
1252class HasVirtualMFA(Filter):
1253 """Is the account configured with a virtual MFA device?
1255 :example:
1257 .. code-block:: yaml
1259 policies:
1260 - name: account-with-virtual-mfa
1261 resource: account
1262 region: us-east-1
1263 filters:
1264 - type: has-virtual-mfa
1265 value: true
1266 """
1268 schema = type_schema('has-virtual-mfa', **{'value': {'type': 'boolean'}})
1270 permissions = ('iam:ListVirtualMFADevices',)
1272 def mfa_belongs_to_root_account(self, mfa):
1273 return mfa['SerialNumber'].endswith(':mfa/root-account-mfa-device')
1275 def account_has_virtual_mfa(self, account):
1276 if not account.get('c7n:VirtualMFADevices'):
1277 client = local_session(self.manager.session_factory).client('iam')
1278 paginator = client.get_paginator('list_virtual_mfa_devices')
1279 raw_list = paginator.paginate().build_full_result()['VirtualMFADevices']
1280 account['c7n:VirtualMFADevices'] = list(filter(
1281 self.mfa_belongs_to_root_account, raw_list))
1282 expect_virtual_mfa = self.data.get('value', True)
1283 has_virtual_mfa = any(account['c7n:VirtualMFADevices'])
1284 return expect_virtual_mfa == has_virtual_mfa
1286 def process(self, resources, event=None):
1287 return list(filter(self.account_has_virtual_mfa, resources))
1290@actions.register('enable-data-events')
1291class EnableDataEvents(BaseAction):
1292 """Ensure all buckets in account are setup to log data events.
1294 Note this works via a single trail for data events per
1295 https://aws.amazon.com/about-aws/whats-new/2017/09/aws-cloudtrail-enables-option-to-add-all-amazon-s3-buckets-to-data-events/
1297 This trail should NOT be used for api management events, the
1298 configuration here is soley for data events. If directed to create
1299 a trail this will do so without management events.
1301 :example:
1303 .. code-block:: yaml
1305 policies:
1306 - name: s3-enable-data-events-logging
1307 resource: account
1308 actions:
1309 - type: enable-data-events
1310 data-trail:
1311 name: s3-events
1312 multi-region: us-east-1
1313 """
1315 schema = type_schema(
1316 'enable-data-events', required=['data-trail'], **{
1317 'data-trail': {
1318 'type': 'object',
1319 'additionalProperties': False,
1320 'required': ['name'],
1321 'properties': {
1322 'create': {
1323 'title': 'Should we create trail if needed for events?',
1324 'type': 'boolean'},
1325 'type': {'enum': ['ReadOnly', 'WriteOnly', 'All']},
1326 'name': {
1327 'title': 'The name of the event trail',
1328 'type': 'string'},
1329 'topic': {
1330 'title': 'If creating, the sns topic for the trail to send updates',
1331 'type': 'string'},
1332 's3-bucket': {
1333 'title': 'If creating, the bucket to store trail event data',
1334 'type': 'string'},
1335 's3-prefix': {'type': 'string'},
1336 'key-id': {
1337 'title': 'If creating, Enable kms on the trail',
1338 'type': 'string'},
1339 # region that we're aggregating via trails.
1340 'multi-region': {
1341 'title': 'If creating, use this region for all data trails',
1342 'type': 'string'}}}})
1344 def validate(self):
1345 if self.data['data-trail'].get('create'):
1346 if 's3-bucket' not in self.data['data-trail']:
1347 raise PolicyValidationError(
1348 "If creating data trails, an s3-bucket is required on %s" % (
1349 self.manager.data))
1350 return self
1352 def get_permissions(self):
1353 perms = [
1354 'cloudtrail:DescribeTrails',
1355 'cloudtrail:GetEventSelectors',
1356 'cloudtrail:PutEventSelectors']
1358 if self.data.get('data-trail', {}).get('create'):
1359 perms.extend([
1360 'cloudtrail:CreateTrail', 'cloudtrail:StartLogging'])
1361 return perms
1363 def add_data_trail(self, client, trail_cfg):
1364 if not trail_cfg.get('create'):
1365 raise ValueError(
1366 "s3 data event trail missing and not configured to create")
1367 params = dict(
1368 Name=trail_cfg['name'],
1369 S3BucketName=trail_cfg['s3-bucket'],
1370 EnableLogFileValidation=True)
1372 if 'key-id' in trail_cfg:
1373 params['KmsKeyId'] = trail_cfg['key-id']
1374 if 's3-prefix' in trail_cfg:
1375 params['S3KeyPrefix'] = trail_cfg['s3-prefix']
1376 if 'topic' in trail_cfg:
1377 params['SnsTopicName'] = trail_cfg['topic']
1378 if 'multi-region' in trail_cfg:
1379 params['IsMultiRegionTrail'] = True
1381 client.create_trail(**params)
1382 return {'Name': trail_cfg['name']}
1384 def process(self, resources):
1385 session = local_session(self.manager.session_factory)
1386 region = self.data['data-trail'].get('multi-region')
1388 if region:
1389 client = session.client('cloudtrail', region_name=region)
1390 else:
1391 client = session.client('cloudtrail')
1393 added = False
1394 tconfig = self.data['data-trail']
1395 trails = client.describe_trails(
1396 trailNameList=[tconfig['name']]).get('trailList', ())
1397 if not trails:
1398 trail = self.add_data_trail(client, tconfig)
1399 added = True
1400 else:
1401 trail = trails[0]
1403 events = client.get_event_selectors(
1404 TrailName=trail['Name']).get('EventSelectors', [])
1406 for e in events:
1407 found = False
1408 if not e.get('DataResources'):
1409 continue
1410 for data_events in e['DataResources']:
1411 if data_events['Type'] != 'AWS::S3::Object':
1412 continue
1413 for b in data_events['Values']:
1414 if b.rsplit(':')[-1].strip('/') == '':
1415 found = True
1416 break
1417 if found:
1418 resources[0]['c7n_data_trail'] = trail
1419 return
1421 # Opinionated choice, separate api and data events.
1422 event_count = len(events)
1423 events = [e for e in events if not e.get('IncludeManagementEvents')]
1424 if len(events) != event_count:
1425 self.log.warning("removing api trail from data trail")
1427 # future proof'd for other data events, for s3 this trail
1428 # encompasses all the buckets in the account.
1430 events.append({
1431 'IncludeManagementEvents': False,
1432 'ReadWriteType': tconfig.get('type', 'All'),
1433 'DataResources': [{
1434 'Type': 'AWS::S3::Object',
1435 'Values': ['arn:aws:s3:::']}]})
1436 client.put_event_selectors(
1437 TrailName=trail['Name'],
1438 EventSelectors=events)
1440 if added:
1441 client.start_logging(Name=tconfig['name'])
1443 resources[0]['c7n_data_trail'] = trail
1446@filters.register('shield-enabled')
1447class ShieldEnabled(Filter):
1449 permissions = ('shield:DescribeSubscription',)
1451 schema = type_schema(
1452 'shield-enabled',
1453 state={'type': 'boolean'})
1455 def process(self, resources, event=None):
1456 state = self.data.get('state', False)
1457 client = local_session(self.manager.session_factory).client('shield')
1458 try:
1459 subscription = client.describe_subscription().get(
1460 'Subscription', None)
1461 except ClientError as e:
1462 if e.response['Error']['Code'] != 'ResourceNotFoundException':
1463 raise
1464 subscription = None
1466 resources[0]['c7n:ShieldSubscription'] = subscription
1467 if state and subscription:
1468 return resources
1469 elif not state and not subscription:
1470 return resources
1471 return []
1474@actions.register('set-shield-advanced')
1475class SetShieldAdvanced(BaseAction):
1476 """Enable/disable Shield Advanced on an account."""
1478 permissions = (
1479 'shield:CreateSubscription', 'shield:DeleteSubscription')
1481 schema = type_schema(
1482 'set-shield-advanced',
1483 state={'type': 'boolean'})
1485 def process(self, resources):
1486 client = local_session(self.manager.session_factory).client('shield')
1487 state = self.data.get('state', True)
1489 if state:
1490 client.create_subscription()
1491 else:
1492 try:
1493 client.delete_subscription()
1494 except ClientError as e:
1495 if e.response['Error']['Code'] == 'ResourceNotFoundException':
1496 return
1497 raise
1500@filters.register('xray-encrypt-key')
1501class XrayEncrypted(Filter):
1502 """Determine if xray is encrypted.
1504 :example:
1506 .. code-block:: yaml
1508 policies:
1509 - name: xray-encrypt-with-default
1510 resource: aws.account
1511 filters:
1512 - type: xray-encrypt-key
1513 key: default
1514 - name: xray-encrypt-with-kms
1515 resource: aws.account
1516 filters:
1517 - type: xray-encrypt-key
1518 key: kms
1519 - name: xray-encrypt-with-specific-key
1520 resource: aws.account
1521 filters:
1522 - type: xray-encrypt-key
1523 key: alias/my-alias or arn or keyid
1524 """
1526 permissions = ('xray:GetEncryptionConfig',)
1527 schema = type_schema(
1528 'xray-encrypt-key',
1529 required=['key'],
1530 key={'type': 'string'}
1531 )
1533 def process(self, resources, event=None):
1534 client = self.manager.session_factory().client('xray')
1535 gec_result = client.get_encryption_config()['EncryptionConfig']
1536 resources[0]['c7n:XrayEncryptionConfig'] = gec_result
1538 k = self.data.get('key')
1539 if k not in ['default', 'kms']:
1540 kmsclient = self.manager.session_factory().client('kms')
1541 keyid = kmsclient.describe_key(KeyId=k)['KeyMetadata']['Arn']
1542 rc = resources if (gec_result['KeyId'] == keyid) else []
1543 else:
1544 kv = 'KMS' if self.data.get('key') == 'kms' else 'NONE'
1545 rc = resources if (gec_result['Type'] == kv) else []
1546 return rc
1549@actions.register('set-xray-encrypt')
1550class SetXrayEncryption(BaseAction):
1551 """Enable specific xray encryption.
1553 :example:
1555 .. code-block:: yaml
1557 policies:
1558 - name: xray-default-encrypt
1559 resource: aws.account
1560 actions:
1561 - type: set-xray-encrypt
1562 key: default
1563 - name: xray-kms-encrypt
1564 resource: aws.account
1565 actions:
1566 - type: set-xray-encrypt
1567 key: alias/some/alias/key
1568 """
1570 permissions = ('xray:PutEncryptionConfig',)
1571 schema = type_schema(
1572 'set-xray-encrypt',
1573 required=['key'],
1574 key={'type': 'string'}
1575 )
1577 def process(self, resources):
1578 client = local_session(self.manager.session_factory).client('xray')
1579 key = self.data.get('key')
1580 req = {'Type': 'NONE'} if key == 'default' else {'Type': 'KMS', 'KeyId': key}
1581 client.put_encryption_config(**req)
1584@filters.register('default-ebs-encryption')
1585class EbsEncryption(Filter):
1586 """Filter an account by its ebs encryption status.
1588 By default for key we match on the alias name for a key.
1590 :example:
1592 .. code-block:: yaml
1594 policies:
1595 - name: check-default-ebs-encryption
1596 resource: aws.account
1597 filters:
1598 - type: default-ebs-encryption
1599 key: "alias/aws/ebs"
1600 state: true
1602 It is also possible to match on specific key attributes (tags, origin)
1604 :example:
1606 .. code-block:: yaml
1608 policies:
1609 - name: check-ebs-encryption-key-origin
1610 resource: aws.account
1611 filters:
1612 - type: default-ebs-encryption
1613 key:
1614 type: value
1615 key: Origin
1616 value: AWS_KMS
1617 state: true
1618 """
1619 permissions = ('ec2:GetEbsEncryptionByDefault',)
1620 schema = type_schema(
1621 'default-ebs-encryption',
1622 state={'type': 'boolean'},
1623 key={'oneOf': [
1624 {'$ref': '#/definitions/filters/value'},
1625 {'type': 'string'}]})
1627 def process(self, resources, event=None):
1628 state = self.data.get('state', False)
1629 client = local_session(self.manager.session_factory).client('ec2')
1630 account_state = client.get_ebs_encryption_by_default().get(
1631 'EbsEncryptionByDefault')
1632 if account_state != state:
1633 return []
1634 if state and 'key' in self.data:
1635 vfd = (isinstance(self.data['key'], dict) and
1636 self.data['key'] or {'c7n:AliasName': self.data['key']})
1637 vf = KmsRelatedFilter(vfd, self.manager)
1638 vf.RelatedIdsExpression = 'KmsKeyId'
1639 vf.annotate = False
1640 key = client.get_ebs_default_kms_key_id().get('KmsKeyId')
1641 if not vf.process([{'KmsKeyId': key}]):
1642 return []
1643 return resources
1646@actions.register('set-ebs-encryption')
1647class SetEbsEncryption(BaseAction):
1648 """Set AWS EBS default encryption on an account
1650 :example:
1652 .. code-block:: yaml
1654 policies:
1655 - name: set-default-ebs-encryption
1656 resource: aws.account
1657 filters:
1658 - type: default-ebs-encryption
1659 state: false
1660 actions:
1661 - type: set-ebs-encryption
1662 state: true
1663 key: alias/aws/ebs
1664 """
1665 permissions = ('ec2:EnableEbsEncryptionByDefault',
1666 'ec2:DisableEbsEncryptionByDefault')
1668 schema = type_schema(
1669 'set-ebs-encryption',
1670 state={'type': 'boolean'},
1671 key={'type': 'string'})
1673 def process(self, resources):
1674 client = local_session(
1675 self.manager.session_factory).client('ec2')
1676 state = self.data.get('state')
1677 key = self.data.get('key')
1678 if state:
1679 client.enable_ebs_encryption_by_default()
1680 else:
1681 client.disable_ebs_encryption_by_default()
1683 if state and key:
1684 client.modify_ebs_default_kms_key_id(
1685 KmsKeyId=self.data['key'])
1688@filters.register('s3-public-block')
1689class S3PublicBlock(ValueFilter):
1690 """Check for s3 public blocks on an account.
1692 https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html
1693 """
1695 annotation_key = 'c7n:s3-public-block'
1696 annotate = False # no annotation from value filter
1697 schema = type_schema('s3-public-block', rinherit=ValueFilter.schema)
1698 schema_alias = False
1699 permissions = ('s3:GetAccountPublicAccessBlock',)
1701 def process(self, resources, event=None):
1702 self.augment([r for r in resources if self.annotation_key not in r])
1703 return super(S3PublicBlock, self).process(resources, event)
1705 def augment(self, resources):
1706 client = local_session(self.manager.session_factory).client('s3control')
1707 for r in resources:
1708 try:
1709 r[self.annotation_key] = client.get_public_access_block(
1710 AccountId=r['account_id']).get('PublicAccessBlockConfiguration', {})
1711 except client.exceptions.NoSuchPublicAccessBlockConfiguration:
1712 r[self.annotation_key] = {}
1714 def __call__(self, r):
1715 return super(S3PublicBlock, self).__call__(r[self.annotation_key])
1718@actions.register('set-s3-public-block')
1719class SetS3PublicBlock(BaseAction):
1720 """Configure S3 Public Access Block on an account.
1722 All public access block attributes can be set. If not specified they are merged
1723 with the extant configuration.
1725 https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html
1727 :example:
1729 .. yaml:
1731 policies:
1732 - name: restrict-public-buckets
1733 resource: aws.account
1734 filters:
1735 - not:
1736 - type: s3-public-block
1737 key: RestrictPublicBuckets
1738 value: true
1739 actions:
1740 - type: set-s3-public-block
1741 RestrictPublicBuckets: true
1743 """
1744 schema = type_schema(
1745 'set-s3-public-block',
1746 state={'type': 'boolean', 'default': True},
1747 BlockPublicAcls={'type': 'boolean'},
1748 IgnorePublicAcls={'type': 'boolean'},
1749 BlockPublicPolicy={'type': 'boolean'},
1750 RestrictPublicBuckets={'type': 'boolean'})
1752 permissions = ('s3:PutAccountPublicAccessBlock', 's3:GetAccountPublicAccessBlock')
1754 def validate(self):
1755 config = self.data.copy()
1756 config.pop('type')
1757 if config.pop('state', None) is False and config:
1758 raise PolicyValidationError(
1759 "{} cant set state false with controls specified".format(
1760 self.type))
1762 def process(self, resources):
1763 client = local_session(self.manager.session_factory).client('s3control')
1764 if self.data.get('state', True) is False:
1765 for r in resources:
1766 client.delete_public_access_block(AccountId=r['account_id'])
1767 return
1769 keys = (
1770 'BlockPublicPolicy', 'BlockPublicAcls', 'IgnorePublicAcls', 'RestrictPublicBuckets')
1772 for r in resources:
1773 # try to merge with existing configuration if not explicitly set.
1774 base = {}
1775 if S3PublicBlock.annotation_key in r:
1776 base = r[S3PublicBlock.annotation_key]
1777 else:
1778 try:
1779 base = client.get_public_access_block(AccountId=r['account_id']).get(
1780 'PublicAccessBlockConfiguration')
1781 except client.exceptions.NoSuchPublicAccessBlockConfiguration:
1782 base = {}
1784 config = {}
1785 for k in keys:
1786 if k in self.data:
1787 config[k] = self.data[k]
1788 elif k in base:
1789 config[k] = base[k]
1791 client.put_public_access_block(
1792 AccountId=r['account_id'],
1793 PublicAccessBlockConfiguration=config)
1796class GlueCatalogEncryptionEnabled(MultiAttrFilter):
1797 """ Filter glue catalog by its glue encryption status and KMS key
1799 :example:
1801 .. code-block:: yaml
1803 policies:
1804 - name: glue-catalog-security-config
1805 resource: aws.glue-catalog
1806 filters:
1807 - type: glue-security-config
1808 SseAwsKmsKeyId: alias/aws/glue
1810 """
1811 retry = staticmethod(QueryResourceManager.retry)
1813 schema = {
1814 'type': 'object',
1815 'additionalProperties': False,
1816 'properties': {
1817 'type': {'enum': ['glue-security-config']},
1818 'CatalogEncryptionMode': {'enum': ['DISABLED', 'SSE-KMS']},
1819 'SseAwsKmsKeyId': {'type': 'string'},
1820 'ReturnConnectionPasswordEncrypted': {'type': 'boolean'},
1821 'AwsKmsKeyId': {'type': 'string'}
1822 }
1823 }
1825 annotation = "c7n:glue-security-config"
1826 permissions = ('glue:GetDataCatalogEncryptionSettings',)
1828 def validate(self):
1829 attrs = set()
1830 for key in self.data:
1831 if key in ['CatalogEncryptionMode',
1832 'ReturnConnectionPasswordEncrypted',
1833 'SseAwsKmsKeyId',
1834 'AwsKmsKeyId']:
1835 attrs.add(key)
1836 self.multi_attrs = attrs
1837 return super(GlueCatalogEncryptionEnabled, self).validate()
1839 def get_target(self, resource):
1840 if self.annotation in resource:
1841 return resource[self.annotation]
1842 client = local_session(self.manager.session_factory).client('glue')
1843 encryption_setting = resource.get('DataCatalogEncryptionSettings')
1844 if self.manager.type != 'glue-catalog':
1845 encryption_setting = client.get_data_catalog_encryption_settings().get(
1846 'DataCatalogEncryptionSettings')
1847 resource[self.annotation] = encryption_setting.get('EncryptionAtRest')
1848 resource[self.annotation].update(encryption_setting.get('ConnectionPasswordEncryption'))
1849 key_attrs = ('SseAwsKmsKeyId', 'AwsKmsKeyId')
1850 for encrypt_attr in key_attrs:
1851 if encrypt_attr not in self.data or not self.data[encrypt_attr].startswith('alias'):
1852 continue
1853 key = resource[self.annotation].get(encrypt_attr)
1854 vfd = {'c7n:AliasName': self.data[encrypt_attr]}
1855 vf = KmsRelatedFilter(vfd, self.manager)
1856 vf.RelatedIdsExpression = 'KmsKeyId'
1857 vf.annotate = False
1858 if not vf.process([{'KmsKeyId': key}]):
1859 return []
1860 resource[self.annotation][encrypt_attr] = self.data[encrypt_attr]
1861 return resource[self.annotation]
1864@filters.register('glue-security-config')
1865class AccountCatalogEncryptionFilter(GlueCatalogEncryptionEnabled):
1866 """Filter aws account by its glue encryption status and KMS key
1868 :example:
1870 .. code-block:: yaml
1872 policies:
1873 - name: glue-security-config
1874 resource: aws.account
1875 filters:
1876 - type: glue-security-config
1877 SseAwsKmsKeyId: alias/aws/glue
1879 """
1882@filters.register('emr-block-public-access')
1883class EMRBlockPublicAccessConfiguration(ValueFilter):
1884 """Check for EMR block public access configuration on an account
1886 :example:
1888 .. code-block:: yaml
1890 policies:
1891 - name: get-emr-block-public-access
1892 resource: account
1893 filters:
1894 - type: emr-block-public-access
1895 """
1897 annotation_key = 'c7n:emr-block-public-access'
1898 annotate = False # no annotation from value filter
1899 schema = type_schema('emr-block-public-access', rinherit=ValueFilter.schema)
1900 schema_alias = False
1901 permissions = ("elasticmapreduce:GetBlockPublicAccessConfiguration",)
1903 def process(self, resources, event=None):
1904 self.augment([r for r in resources if self.annotation_key not in r])
1905 return super().process(resources, event)
1907 def augment(self, resources):
1908 client = local_session(self.manager.session_factory).client(
1909 'emr', region_name=self.manager.config.region)
1911 for r in resources:
1912 try:
1913 r[self.annotation_key] = client.get_block_public_access_configuration()
1914 r[self.annotation_key].pop('ResponseMetadata')
1915 except client.exceptions.NoSuchPublicAccessBlockConfiguration:
1916 r[self.annotation_key] = {}
1918 def __call__(self, r):
1919 return super(EMRBlockPublicAccessConfiguration, self).__call__(r[self.annotation_key])
1922@actions.register('set-emr-block-public-access')
1923class PutAccountBlockPublicAccessConfiguration(BaseAction):
1924 """Action to put/update the EMR block public access configuration for your
1925 AWS account in the current region
1927 :example:
1929 .. code-block:: yaml
1931 policies:
1932 - name: set-emr-block-public-access
1933 resource: account
1934 filters:
1935 - type: emr-block-public-access
1936 key: BlockPublicAccessConfiguration.BlockPublicSecurityGroupRules
1937 value: False
1938 actions:
1939 - type: set-emr-block-public-access
1940 config:
1941 BlockPublicSecurityGroupRules: True
1942 PermittedPublicSecurityGroupRuleRanges:
1943 - MinRange: 22
1944 MaxRange: 22
1945 - MinRange: 23
1946 MaxRange: 23
1948 """
1950 schema = type_schema('set-emr-block-public-access',
1951 config={"type": "object",
1952 'properties': {
1953 'BlockPublicSecurityGroupRules': {'type': 'boolean'},
1954 'PermittedPublicSecurityGroupRuleRanges': {
1955 'type': 'array',
1956 'items': {
1957 'type': 'object',
1958 'properties': {
1959 'MinRange': {'type': 'number', "minimum": 0},
1960 'MaxRange': {'type': 'number', "minimum": 0}
1961 },
1962 'required': ['MinRange']
1963 }
1964 }
1965 },
1966 'required': ['BlockPublicSecurityGroupRules']
1967 },
1968 required=('config',))
1970 permissions = ("elasticmapreduce:PutBlockPublicAccessConfiguration",)
1972 def process(self, resources):
1973 client = local_session(self.manager.session_factory).client('emr')
1974 r = resources[0]
1976 base = {}
1977 if EMRBlockPublicAccessConfiguration.annotation_key in r:
1978 base = r[EMRBlockPublicAccessConfiguration.annotation_key]
1979 else:
1980 try:
1981 base = client.get_block_public_access_configuration()
1982 base.pop('ResponseMetadata')
1983 except client.exceptions.NoSuchPublicAccessBlockConfiguration:
1984 base = {}
1986 config = base['BlockPublicAccessConfiguration']
1987 updatedConfig = {**config, **self.data.get('config')}
1989 if config == updatedConfig:
1990 return
1992 client.put_block_public_access_configuration(
1993 BlockPublicAccessConfiguration=updatedConfig
1994 )
1997@filters.register('securityhub')
1998class SecHubEnabled(Filter):
1999 """Filter an account depending on whether security hub is enabled or not.
2001 :example:
2003 .. code-block:: yaml
2005 policies:
2006 - name: check-securityhub-status
2007 resource: aws.account
2008 filters:
2009 - type: securityhub
2010 enabled: true
2012 """
2014 permissions = ('securityhub:DescribeHub',)
2016 schema = type_schema('securityhub', enabled={'type': 'boolean'})
2018 def process(self, resources, event=None):
2019 state = self.data.get('enabled', True)
2020 client = local_session(self.manager.session_factory).client('securityhub')
2021 sechub = self.manager.retry(client.describe_hub, ignore_err_codes=(
2022 'InvalidAccessException',))
2023 if state == bool(sechub):
2024 return resources
2025 return []
2028@filters.register('lakeformation-s3-cross-account')
2029class LakeformationFilter(Filter):
2030 """Flags an account if its using a lakeformation s3 bucket resource from a different account.
2032 :example:
2034 .. code-block:: yaml
2036 policies:
2037 - name: lakeformation-cross-account-bucket
2038 resource: aws.account
2039 filters:
2040 - type: lakeformation-s3-cross-account
2042 """
2044 schema = type_schema('lakeformation-s3-cross-account', rinherit=ValueFilter.schema)
2045 schema_alias = False
2046 permissions = ('lakeformation:ListResources',)
2047 annotation = 'c7n:lake-cross-account-s3'
2049 def process(self, resources, event=None):
2050 results = []
2051 for r in resources:
2052 if self.process_account(r):
2053 results.append(r)
2054 return results
2056 def process_account(self, account):
2057 client = local_session(self.manager.session_factory).client('lakeformation')
2058 lake_buckets = {
2059 Arn.parse(r).resource for r in jmespath_search(
2060 'ResourceInfoList[].ResourceArn',
2061 client.list_resources())
2062 }
2063 buckets = {
2064 b['Name'] for b in
2065 self.manager.get_resource_manager('s3').resources(augment=False)}
2066 cross_account = lake_buckets.difference(buckets)
2067 if not cross_account:
2068 return False
2069 account[self.annotation] = list(cross_account)
2070 return True
2073@actions.register('toggle-config-managed-rule')
2074class ToggleConfigManagedRule(BaseAction):
2075 """Enables or disables an AWS Config Managed Rule
2077 :example:
2079 .. code-block:: yaml
2081 policies:
2082 - name: config-managed-s3-bucket-public-write-remediate-event
2083 description: |
2084 This policy detects if S3 bucket allows public write by the bucket policy
2085 or ACL and remediates.
2086 comment: |
2087 This policy detects if S3 bucket policy or ACL allows public write access.
2088 When the bucket is evaluated as 'NON_COMPLIANT', the action
2089 'AWS-DisableS3BucketPublicReadWrite' is triggered and remediates.
2090 resource: account
2091 filters:
2092 - type: missing
2093 policy:
2094 resource: config-rule
2095 filters:
2096 - type: remediation
2097 rule_name: &rule_name 'config-managed-s3-bucket-public-write-remediate-event'
2098 remediation: &remediation-config
2099 TargetId: AWS-DisableS3BucketPublicReadWrite
2100 Automatic: true
2101 MaximumAutomaticAttempts: 5
2102 RetryAttemptSeconds: 211
2103 Parameters:
2104 AutomationAssumeRole:
2105 StaticValue:
2106 Values:
2107 - 'arn:aws:iam::{account_id}:role/myrole'
2108 S3BucketName:
2109 ResourceValue:
2110 Value: RESOURCE_ID
2111 actions:
2112 - type: toggle-config-managed-rule
2113 rule_name: *rule_name
2114 managed_rule_id: S3_BUCKET_PUBLIC_WRITE_PROHIBITED
2115 resource_types:
2116 - 'AWS::S3::Bucket'
2117 rule_parameters: '{}'
2118 remediation: *remediation-config
2119 """
2121 permissions = (
2122 'config:DescribeConfigRules',
2123 'config:DescribeRemediationConfigurations',
2124 'config:PutRemediationConfigurations',
2125 'config:PutConfigRule',
2126 )
2128 schema = type_schema('toggle-config-managed-rule',
2129 enabled={'type': 'boolean', 'default': True},
2130 rule_name={'type': 'string'},
2131 rule_prefix={'type': 'string'},
2132 managed_rule_id={'type': 'string'},
2133 resource_types={'type': 'array', 'items':
2134 {'pattern': '^AWS::*', 'type': 'string'}},
2135 resource_tag={
2136 'type': 'object',
2137 'properties': {
2138 'key': {'type': 'string'},
2139 'value': {'type': 'string'},
2140 },
2141 'required': ['key', 'value'],
2142 },
2143 resource_id={'type': 'string'},
2144 rule_parameters={'type': 'string'},
2145 remediation={
2146 'type': 'object',
2147 'properties': {
2148 'TargetType': {'type': 'string'},
2149 'TargetId': {'type': 'string'},
2150 'Automatic': {'type': 'boolean'},
2151 'Parameters': {'type': 'object'},
2152 'MaximumAutomaticAttempts': {
2153 'type': 'integer',
2154 'minimum': 1, 'maximum': 25,
2155 },
2156 'RetryAttemptSeconds': {
2157 'type': 'integer',
2158 'minimum': 1, 'maximum': 2678000,
2159 },
2160 'ExecutionControls': {'type': 'object'},
2161 },
2162 },
2163 tags={'type': 'object'},
2164 required=['rule_name'],
2165 )
2167 def validate(self):
2168 if (
2169 self.data.get('enabled', True) and
2170 not self.data.get('managed_rule_id')
2171 ):
2172 raise PolicyValidationError("managed_rule_id required to enable a managed rule")
2173 return self
2175 def process(self, accounts):
2176 client = local_session(self.manager.session_factory).client('config')
2177 rule = self.ConfigManagedRule(self.data)
2178 params = self.get_rule_params(rule)
2180 if self.data.get('enabled', True):
2181 client.put_config_rule(**params)
2183 if rule.remediation:
2184 remediation_params = self.get_remediation_params(rule)
2185 client.put_remediation_configurations(
2186 RemediationConfigurations=[remediation_params]
2187 )
2188 else:
2189 with suppress(client.exceptions.NoSuchRemediationConfigurationException):
2190 client.delete_remediation_configuration(
2191 ConfigRuleName=rule.name
2192 )
2194 with suppress(client.exceptions.NoSuchConfigRuleException):
2195 client.delete_config_rule(
2196 ConfigRuleName=rule.name
2197 )
2199 def get_rule_params(self, rule):
2200 params = dict(
2201 ConfigRuleName=rule.name,
2202 Description=rule.description,
2203 Source={
2204 'Owner': 'AWS',
2205 'SourceIdentifier': rule.managed_rule_id,
2206 },
2207 InputParameters=rule.rule_parameters
2208 )
2210 # A config rule scope can include one or more resource types,
2211 # a combination of a tag key and value, or a combination of
2212 # one resource type and one resource ID
2213 params.update({'Scope': {'ComplianceResourceTypes': rule.resource_types}})
2214 if rule.resource_tag:
2215 params.update({'Scope': {
2216 'TagKey': rule.resource_tag['key'],
2217 'TagValue': rule.resource_tag['value']}
2218 })
2219 elif rule.resource_id:
2220 params.update({'Scope': {'ComplianceResourceId': rule.resource_id}})
2222 return dict(ConfigRule=params)
2224 def get_remediation_params(self, rule):
2225 rule.remediation['ConfigRuleName'] = rule.name
2226 if 'TargetType' not in rule.remediation:
2227 rule.remediation['TargetType'] = 'SSM_DOCUMENT'
2228 return rule.remediation
2230 class ConfigManagedRule:
2231 """Wraps the action data into an AWS Config Managed Rule.
2232 """
2234 def __init__(self, data):
2235 self.data = data
2237 @property
2238 def name(self):
2239 prefix = self.data.get('rule_prefix', 'custodian-')
2240 return "%s%s" % (prefix, self.data.get('rule_name', ''))
2242 @property
2243 def description(self):
2244 return self.data.get(
2245 'description', 'cloud-custodian AWS Config Managed Rule policy')
2247 @property
2248 def tags(self):
2249 return self.data.get('tags', {})
2251 @property
2252 def resource_types(self):
2253 return self.data.get('resource_types', [])
2255 @property
2256 def managed_rule_id(self):
2257 return self.data.get('managed_rule_id', '')
2259 @property
2260 def resource_tag(self):
2261 return self.data.get('resource_tag', {})
2263 @property
2264 def resource_id(self):
2265 return self.data.get('resource_id', '')
2267 @property
2268 def rule_parameters(self):
2269 return self.data.get('rule_parameters', '')
2271 @property
2272 def remediation(self):
2273 return self.data.get('remediation', {})
2276@filters.register('ses-agg-send-stats')
2277class SesAggStats(ValueFilter):
2278 """This filter queries SES send statistics and aggregates all
2279 the data points into a single report.
2281 :example:
2283 .. code-block:: yaml
2285 policies:
2286 - name: ses-aggregated-send-stats-policy
2287 resource: account
2288 filters:
2289 - type: ses-agg-send-stats
2290 """
2292 schema = type_schema('ses-agg-send-stats', rinherit=ValueFilter.schema)
2293 annotation_key = 'c7n:ses-send-agg'
2294 permissions = ("ses:GetSendStatistics",)
2296 def process(self, resources, event=None):
2297 client = local_session(self.manager.session_factory).client('ses')
2298 get_send_stats = client.get_send_statistics()
2299 results = []
2301 if not get_send_stats or not get_send_stats.get('SendDataPoints'):
2302 return results
2304 resource_counter = {'DeliveryAttempts': 0,
2305 'Bounces': 0,
2306 'Complaints': 0,
2307 'Rejects': 0,
2308 'BounceRate': 0}
2309 for d in get_send_stats.get('SendDataPoints', []):
2310 resource_counter['DeliveryAttempts'] += d['DeliveryAttempts']
2311 resource_counter['Bounces'] += d['Bounces']
2312 resource_counter['Complaints'] += d['Complaints']
2313 resource_counter['Rejects'] += d['Rejects']
2314 resource_counter['BounceRate'] = round(
2315 (resource_counter['Bounces'] /
2316 resource_counter['DeliveryAttempts']) * 100)
2317 resources[0][self.annotation_key] = resource_counter
2319 return resources
2322@filters.register('ses-send-stats')
2323class SesConsecutiveStats(Filter):
2324 """This filter annotates the account resource with SES send statistics for the
2325 last n number of days, not including the current date.
2327 The stats are aggregated into daily metrics. Additionally, the filter also
2328 calculates and annotates the max daily bounce rate (percentage). Using this filter,
2329 users can alert when the bounce rate for a particular day is higher than the limit.
2331 :example:
2333 .. code-block:: yaml
2335 policies:
2336 - name: ses-send-stats
2337 resource: account
2338 filters:
2339 - type: ses-send-stats
2340 days: 5
2341 - type: value
2342 key: '"c7n:ses-max-bounce-rate"'
2343 op: ge
2344 value: 10
2345 """
2346 schema = type_schema('ses-send-stats', days={'type': 'number', 'minimum': 2},
2347 required=['days'])
2348 send_stats_annotation = 'c7n:ses-send-stats'
2349 max_bounce_annotation = 'c7n:ses-max-bounce-rate'
2350 permissions = ("ses:GetSendStatistics",)
2352 def process(self, resources, event=None):
2353 client = local_session(self.manager.session_factory).client('ses')
2354 get_send_stats = client.get_send_statistics()
2355 results = []
2356 check_days = self.data.get('days', 2)
2357 utcnow = datetime.datetime.utcnow()
2358 expected_dates = set()
2360 for days in range(1, check_days + 1):
2361 expected_dates.add((utcnow - datetime.timedelta(days=days)).strftime('%Y-%m-%d'))
2363 if not get_send_stats or not get_send_stats.get('SendDataPoints'):
2364 return results
2366 metrics = {}
2367 for d in get_send_stats.get('SendDataPoints', []):
2368 ts = d['Timestamp'].strftime('%Y-%m-%d')
2369 if ts not in expected_dates:
2370 continue
2372 if not metrics.get(ts):
2373 metrics[ts] = {'DeliveryAttempts': 0,
2374 'Bounces': 0,
2375 'Complaints': 0,
2376 'Rejects': 0}
2377 metrics[ts]['DeliveryAttempts'] += d['DeliveryAttempts']
2378 metrics[ts]['Bounces'] += d['Bounces']
2379 metrics[ts]['Complaints'] += d['Complaints']
2380 metrics[ts]['Rejects'] += d['Rejects']
2382 max_bounce_rate = 0
2383 for ts, metric in metrics.items():
2384 metric['BounceRate'] = round((metric['Bounces'] / metric['DeliveryAttempts']) * 100)
2385 if max_bounce_rate < metric['BounceRate']:
2386 max_bounce_rate = metric['BounceRate']
2387 metric['Date'] = ts
2389 resources[0][self.send_stats_annotation] = list(metrics.values())
2390 resources[0][self.max_bounce_annotation] = max_bounce_rate
2392 return resources