Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/c7n/resources/account.py: 29%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 resources
21from c7n.utils import local_session, type_schema, generate_arn, get_support_region, jmespath_search
22from c7n.query import QueryResourceManager, TypeInfo, DescribeSource
23from c7n.filters import ListItemFilter
25from c7n.resources.iam import CredentialReport
26from c7n.resources.securityhub import OtherResourcePostFinding
28from .aws import shape_validate, Arn
30filters = FilterRegistry('aws.account.filters')
31actions = ActionRegistry('aws.account.actions')
33retry = staticmethod(QueryResourceManager.retry)
34filters.register('missing', Missing)
37class DescribeAccount(DescribeSource):
39 def get_account(self):
40 client = local_session(self.manager.session_factory).client("iam")
41 aliases = client.list_account_aliases().get(
42 'AccountAliases', ('',))
43 name = aliases and aliases[0] or ""
44 return {'account_id': self.manager.config.account_id,
45 'account_name': name}
47 def resources(self, query=None):
48 return [self.get_account()]
50 def get_resources(self, resource_ids):
51 return [self.get_account()]
54@resources.register('account')
55class Account(QueryResourceManager):
57 filter_registry = filters
58 action_registry = actions
59 retry = staticmethod(QueryResourceManager.retry)
60 source_type = 'describe'
62 class resource_type(TypeInfo):
63 id = 'account_id'
64 name = 'account_name'
65 filter_name = None
66 global_resource = True
67 # fake this for doc gen
68 service = "account"
69 # for posting config rule evaluations
70 cfn_type = 'AWS::::Account'
72 source_mapping = {
73 'describe': DescribeAccount
74 }
76 @classmethod
77 def get_permissions(cls):
78 return ('iam:ListAccountAliases',)
80 @classmethod
81 def has_arn(cls):
82 return True
84 def get_arns(self, resources):
85 return ["arn:::{account_id}".format(**r) for r in resources]
88@filters.register('credential')
89class AccountCredentialReport(CredentialReport):
91 def process(self, resources, event=None):
92 super(AccountCredentialReport, self).process(resources, event)
93 report = self.get_credential_report()
94 if report is None:
95 return []
96 results = []
97 info = report.get('<root_account>')
98 for r in resources:
99 if self.match(r, info):
100 r['c7n:credential-report'] = info
101 results.append(r)
102 return results
105@filters.register('organization')
106class AccountOrganization(ValueFilter):
107 """Check organization enrollment and configuration
109 :example:
111 determine if an account is not in an organization
113 .. code-block:: yaml
115 policies:
116 - name: no-org
117 resource: account
118 filters:
119 - type: organization
120 key: Id
121 value: absent
124 :example:
126 determine if an account is setup for organization policies
128 .. code-block:: yaml
130 policies:
131 - name: org-policies-not-enabled
132 resource: account
133 filters:
134 - type: organization
135 key: FeatureSet
136 value: ALL
137 op: not-equal
138 """
139 schema = type_schema('organization', rinherit=ValueFilter.schema)
140 schema_alias = False
142 annotation_key = 'c7n:org'
143 annotate = False
145 permissions = ('organizations:DescribeOrganization',)
147 def get_org_info(self, account):
148 client = local_session(
149 self.manager.session_factory).client('organizations')
150 try:
151 org_info = client.describe_organization().get('Organization')
152 except client.exceptions.AWSOrganizationsNotInUseException:
153 org_info = {}
154 except ClientError as e:
155 self.log.warning('organization filter error accessing org info %s', e)
156 org_info = None
157 account[self.annotation_key] = org_info
159 def process(self, resources, event=None):
160 if self.annotation_key not in resources[0]:
161 self.get_org_info(resources[0])
162 # if we can't access org info, we've already logged, and return
163 if resources[0][self.annotation_key] is None:
164 return []
165 if super().process([resources[0][self.annotation_key]]):
166 return resources
167 return []
170@filters.register('check-macie')
171class MacieEnabled(ValueFilter):
172 """Check status of macie v2 in the account.
174 Gets the macie session info for the account, and
175 the macie master account for the current account if
176 configured.
177 """
179 schema = type_schema('check-macie', rinherit=ValueFilter.schema)
180 schema_alias = False
181 annotation_key = 'c7n:macie'
182 annotate = False
183 permissions = ('macie2:GetMacieSession', 'macie2:GetMasterAccount',)
185 def process(self, resources, event=None):
187 if self.annotation_key not in resources[0]:
188 self.get_macie_info(resources[0])
190 if super().process([resources[0][self.annotation_key]]):
191 return resources
193 return []
195 def get_macie_info(self, account):
196 client = local_session(
197 self.manager.session_factory).client('macie2')
199 try:
200 info = client.get_macie_session()
201 info.pop('ResponseMetadata')
202 except client.exceptions.AccessDeniedException:
203 info = {}
205 try:
206 minfo = client.get_master_account().get('master')
207 except (client.exceptions.AccessDeniedException,
208 client.exceptions.ResourceNotFoundException):
209 info['master'] = {}
210 else:
211 info['master'] = minfo
212 account[self.annotation_key] = info
215@filters.register('check-cloudtrail')
216class CloudTrailEnabled(Filter):
217 """Verify cloud trail enabled for this account per specifications.
219 Returns an annotated account resource if trail is not enabled.
221 Of particular note, the current-region option will evaluate whether cloudtrail is available
222 in the current region, either as a multi region trail or as a trail with it as the home region.
224 The log-metric-filter-pattern option checks for the existence of a cloudwatch alarm and a
225 corresponding SNS subscription for a specific filter pattern
227 :example:
229 .. code-block:: yaml
231 policies:
232 - name: account-cloudtrail-enabled
233 resource: account
234 region: us-east-1
235 filters:
236 - type: check-cloudtrail
237 global-events: true
238 multi-region: true
239 running: true
240 include-management-events: true
241 log-metric-filter-pattern: "{ ($.eventName = \\"ConsoleLogin\\") }"
243 Check for CloudWatch log group with a metric filter that has a filter pattern
244 matching a regex pattern:
246 .. code-block:: yaml
248 policies:
249 - name: account-cloudtrail-with-matching-log-metric-filter
250 resource: account
251 region: us-east-1
252 filters:
253 - type: check-cloudtrail
254 log-metric-filter-pattern:
255 type: value
256 op: regex
257 value: '\\{ ?(\\()? ?\\$\\.eventName ?= ?(")?ConsoleLogin(")? ?(\\))? ?\\}'
258 """
259 schema = type_schema(
260 'check-cloudtrail',
261 **{'multi-region': {'type': 'boolean'},
262 'global-events': {'type': 'boolean'},
263 'current-region': {'type': 'boolean'},
264 'running': {'type': 'boolean'},
265 'notifies': {'type': 'boolean'},
266 'file-digest': {'type': 'boolean'},
267 'kms': {'type': 'boolean'},
268 'kms-key': {'type': 'string'},
269 'include-management-events': {'type': 'boolean'},
270 'log-metric-filter-pattern': {'oneOf': [
271 {'$ref': '#/definitions/filters/value'},
272 {'type': 'string'}]}})
274 permissions = ('cloudtrail:DescribeTrails', 'cloudtrail:GetTrailStatus',
275 'cloudtrail:GetEventSelectors', 'cloudwatch:DescribeAlarmsForMetric',
276 'logs:DescribeMetricFilters', 'sns:GetTopicAttributes')
278 def process(self, resources, event=None):
279 session = local_session(self.manager.session_factory)
280 client = session.client('cloudtrail')
281 trails = client.describe_trails()['trailList']
282 resources[0]['c7n:cloudtrails'] = trails
284 if self.data.get('global-events'):
285 trails = [t for t in trails if t.get('IncludeGlobalServiceEvents')]
286 if self.data.get('current-region'):
287 current_region = session.region_name
288 trails = [t for t in trails if t.get(
289 'HomeRegion') == current_region or t.get('IsMultiRegionTrail')]
290 if self.data.get('kms'):
291 trails = [t for t in trails if t.get('KmsKeyId')]
292 if self.data.get('kms-key'):
293 trails = [t for t in trails
294 if t.get('KmsKeyId', '') == self.data['kms-key']]
295 if self.data.get('file-digest'):
296 trails = [t for t in trails
297 if t.get('LogFileValidationEnabled')]
298 if self.data.get('multi-region'):
299 trails = [t for t in trails if t.get('IsMultiRegionTrail')]
300 if self.data.get('notifies'):
301 trails = [t for t in trails if t.get('SnsTopicARN')]
302 if self.data.get('running', True):
303 running = []
304 for t in list(trails):
305 t['Status'] = status = client.get_trail_status(
306 Name=t['TrailARN'])
307 if status['IsLogging'] and not status.get(
308 'LatestDeliveryError'):
309 running.append(t)
310 trails = running
311 if self.data.get('include-management-events'):
312 matched = []
313 for t in list(trails):
314 selectors = client.get_event_selectors(TrailName=t['TrailARN'])
315 if 'EventSelectors' in selectors.keys():
316 for s in selectors['EventSelectors']:
317 if s['IncludeManagementEvents'] and s['ReadWriteType'] == 'All':
318 matched.append(t)
319 elif 'AdvancedEventSelectors' in selectors.keys():
320 for s in selectors['AdvancedEventSelectors']:
321 management = False
322 readonly = False
323 for field_selector in s['FieldSelectors']:
324 if field_selector['Field'] == 'eventCategory' and \
325 'Management' in field_selector['Equals']:
326 management = True
327 elif field_selector['Field'] == 'readOnly':
328 readonly = True
329 if management and not readonly:
330 matched.append(t)
332 trails = matched
333 if self.data.get('log-metric-filter-pattern'):
334 client_logs = session.client('logs')
335 client_cw = session.client('cloudwatch')
336 client_sns = session.client('sns')
337 matched = []
338 pattern = self.data.get('log-metric-filter-pattern')
339 if isinstance(pattern, str):
340 vf = ValueFilter({'key': 'filterPattern', 'value': pattern})
341 else:
342 pattern.setdefault('key', 'filterPattern')
343 vf = ValueFilter(pattern)
345 for t in list(trails):
346 if 'CloudWatchLogsLogGroupArn' not in t.keys():
347 continue
348 log_group_name = t['CloudWatchLogsLogGroupArn'].split(':')[6]
349 try:
350 metric_filters_log_group = \
351 client_logs.describe_metric_filters(
352 logGroupName=log_group_name)['metricFilters']
353 except ClientError as e:
354 if e.response['Error']['Code'] == 'ResourceNotFoundException':
355 continue
356 filter_matched = None
357 if metric_filters_log_group:
358 for f in metric_filters_log_group:
359 if vf(f):
360 filter_matched = f
361 break
362 if not filter_matched:
363 continue
364 alarms = client_cw.describe_alarms_for_metric(
365 MetricName=filter_matched["metricTransformations"][0]["metricName"],
366 Namespace=filter_matched["metricTransformations"][0]["metricNamespace"]
367 )['MetricAlarms']
368 alarm_actions = []
369 for a in alarms:
370 alarm_actions.extend(a['AlarmActions'])
371 if not alarm_actions:
372 continue
373 alarm_actions = set(alarm_actions)
374 for a in alarm_actions:
375 try:
376 sns_topic_attributes = client_sns.get_topic_attributes(TopicArn=a)
377 sns_topic_attributes = sns_topic_attributes.get('Attributes')
378 if sns_topic_attributes.get('SubscriptionsConfirmed', '0') != '0':
379 matched.append(t)
380 except client_sns.exceptions.InvalidParameterValueException:
381 # we can ignore any exception here, the alarm action might
382 # not be an sns topic for instance
383 continue
384 trails = matched
385 if trails:
386 return []
387 return resources
390@filters.register('guard-duty')
391class GuardDutyEnabled(MultiAttrFilter):
392 """Check if the guard duty service is enabled.
394 This allows looking at account's detector and its associated
395 master if any.
397 :example:
399 Check to ensure guard duty is active on account and associated to a master.
401 .. code-block:: yaml
403 policies:
404 - name: guardduty-enabled
405 resource: account
406 filters:
407 - type: guard-duty
408 Detector.Status: ENABLED
409 Master.AccountId: "00011001"
410 Master.RelationshipStatus: "Enabled"
411 """
413 schema = {
414 'type': 'object',
415 'additionalProperties': False,
416 'properties': {
417 'type': {'enum': ['guard-duty']},
418 'match-operator': {'enum': ['or', 'and']}},
419 'patternProperties': {
420 '^Detector': {'oneOf': [{'type': 'object'}, {'type': 'string'}]},
421 '^Master': {'oneOf': [{'type': 'object'}, {'type': 'string'}]}},
422 }
424 annotation = "c7n:guard-duty"
425 permissions = (
426 'guardduty:GetMasterAccount',
427 'guardduty:ListDetectors',
428 'guardduty:GetDetector')
430 def validate(self):
431 attrs = set()
432 for k in self.data:
433 if k.startswith('Detector') or k.startswith('Master'):
434 attrs.add(k)
435 self.multi_attrs = attrs
436 return super(GuardDutyEnabled, self).validate()
438 def get_target(self, resource):
439 if self.annotation in resource:
440 return resource[self.annotation]
442 client = local_session(self.manager.session_factory).client('guardduty')
443 # detectors are singletons too.
444 detector_ids = client.list_detectors().get('DetectorIds')
446 if not detector_ids:
447 return None
448 else:
449 detector_id = detector_ids.pop()
451 detector = client.get_detector(DetectorId=detector_id)
452 detector.pop('ResponseMetadata', None)
453 master = client.get_master_account(DetectorId=detector_id).get('Master')
454 resource[self.annotation] = r = {'Detector': detector, 'Master': master}
455 return r
458@filters.register('check-config')
459class ConfigEnabled(Filter):
460 """Is config service enabled for this account
462 :example:
464 .. code-block:: yaml
466 policies:
467 - name: account-check-config-services
468 resource: account
469 region: us-east-1
470 filters:
471 - type: check-config
472 all-resources: true
473 global-resources: true
474 running: true
475 """
477 schema = type_schema(
478 'check-config', **{
479 'all-resources': {'type': 'boolean'},
480 'running': {'type': 'boolean'},
481 'global-resources': {'type': 'boolean'}})
483 permissions = ('config:DescribeDeliveryChannels',
484 'config:DescribeConfigurationRecorders',
485 'config:DescribeConfigurationRecorderStatus')
487 def process(self, resources, event=None):
488 client = local_session(
489 self.manager.session_factory).client('config')
490 channels = client.describe_delivery_channels()[
491 'DeliveryChannels']
492 recorders = client.describe_configuration_recorders()[
493 'ConfigurationRecorders']
494 resources[0]['c7n:config_recorders'] = recorders
495 resources[0]['c7n:config_channels'] = channels
496 if self.data.get('global-resources'):
497 recorders = [
498 r for r in recorders
499 if r['recordingGroup'].get('includeGlobalResourceTypes')]
500 if self.data.get('all-resources'):
501 recorders = [r for r in recorders
502 if r['recordingGroup'].get('allSupported')]
503 if self.data.get('running', True) and recorders:
504 status = {s['name']: s for
505 s in client.describe_configuration_recorder_status(
506 )['ConfigurationRecordersStatus']}
507 resources[0]['c7n:config_status'] = status
508 recorders = [r for r in recorders if status[r['name']]['recording'] and
509 status[r['name']]['lastStatus'].lower() in ('pending', 'success')]
510 if channels and recorders:
511 return []
512 return resources
515@filters.register('iam-summary')
516class IAMSummary(ValueFilter):
517 """Return annotated account resource if iam summary filter matches.
519 Some use cases include, detecting root api keys or mfa usage.
521 Example iam summary wrt to matchable fields::
523 {
524 "AccessKeysPerUserQuota": 2,
525 "AccountAccessKeysPresent": 0,
526 "AccountMFAEnabled": 1,
527 "AccountSigningCertificatesPresent": 0,
528 "AssumeRolePolicySizeQuota": 2048,
529 "AttachedPoliciesPerGroupQuota": 10,
530 "AttachedPoliciesPerRoleQuota": 10,
531 "AttachedPoliciesPerUserQuota": 10,
532 "GroupPolicySizeQuota": 5120,
533 "Groups": 1,
534 "GroupsPerUserQuota": 10,
535 "GroupsQuota": 100,
536 "InstanceProfiles": 0,
537 "InstanceProfilesQuota": 100,
538 "MFADevices": 3,
539 "MFADevicesInUse": 2,
540 "Policies": 3,
541 "PoliciesQuota": 1000,
542 "PolicySizeQuota": 5120,
543 "PolicyVersionsInUse": 5,
544 "PolicyVersionsInUseQuota": 10000,
545 "Providers": 0,
546 "RolePolicySizeQuota": 10240,
547 "Roles": 4,
548 "RolesQuota": 250,
549 "ServerCertificates": 0,
550 "ServerCertificatesQuota": 20,
551 "SigningCertificatesPerUserQuota": 2,
552 "UserPolicySizeQuota": 2048,
553 "Users": 5,
554 "UsersQuota": 5000,
555 "VersionsPerPolicyQuota": 5,
556 }
558 For example to determine if an account has either not been
559 enabled with root mfa or has root api keys.
561 .. code-block:: yaml
563 policies:
564 - name: root-keys-or-no-mfa
565 resource: account
566 filters:
567 - type: iam-summary
568 key: AccountMFAEnabled
569 value: true
570 op: eq
571 value_type: swap
572 """
573 schema = type_schema('iam-summary', rinherit=ValueFilter.schema)
574 schema_alias = False
575 permissions = ('iam:GetAccountSummary',)
577 def process(self, resources, event=None):
578 if not resources[0].get('c7n:iam_summary'):
579 client = local_session(
580 self.manager.session_factory).client('iam')
581 resources[0]['c7n:iam_summary'] = client.get_account_summary(
582 )['SummaryMap']
583 if self.match(resources[0]['c7n:iam_summary']):
584 return resources
585 return []
588@filters.register('access-analyzer')
589class AccessAnalyzer(ValueFilter):
590 """Check for access analyzers in an account
592 :example:
594 .. code-block:: yaml
596 policies:
597 - name: account-access-analyzer
598 resource: account
599 filters:
600 - type: access-analyzer
601 key: 'status'
602 value: ACTIVE
603 op: eq
604 """
606 schema = type_schema('access-analyzer', rinherit=ValueFilter.schema)
607 schema_alias = False
608 permissions = ('access-analyzer:ListAnalyzers',)
609 annotation_key = 'c7n:matched-analyzers'
611 def process(self, resources, event=None):
612 account = resources[0]
613 if not account.get(self.annotation_key):
614 client = local_session(self.manager.session_factory).client('accessanalyzer')
615 analyzers = self.manager.retry(client.list_analyzers)['analyzers']
616 else:
617 analyzers = account.get(self.annotation_key)
619 matched_analyzers = []
620 for analyzer in analyzers:
621 if self.match(analyzer):
622 matched_analyzers.append(analyzer)
623 account[self.annotation_key] = matched_analyzers
624 return matched_analyzers and resources or []
627@filters.register('password-policy')
628class AccountPasswordPolicy(ValueFilter):
629 """Check an account's password policy.
631 Note that on top of the default password policy fields, we also add an extra key,
632 PasswordPolicyConfigured which will be set to true or false to signify if the given
633 account has attempted to set a policy at all.
635 :example:
637 .. code-block:: yaml
639 policies:
640 - name: password-policy-check
641 resource: account
642 region: us-east-1
643 filters:
644 - type: password-policy
645 key: MinimumPasswordLength
646 value: 10
647 op: ge
648 - type: password-policy
649 key: RequireSymbols
650 value: true
651 """
652 schema = type_schema('password-policy', rinherit=ValueFilter.schema)
653 schema_alias = False
654 permissions = ('iam:GetAccountPasswordPolicy',)
656 def process(self, resources, event=None):
657 account = resources[0]
658 if not account.get('c7n:password_policy'):
659 client = local_session(self.manager.session_factory).client('iam')
660 policy = {}
661 try:
662 policy = client.get_account_password_policy().get('PasswordPolicy', {})
663 policy['PasswordPolicyConfigured'] = True
664 except ClientError as e:
665 if e.response['Error']['Code'] == 'NoSuchEntity':
666 policy['PasswordPolicyConfigured'] = False
667 else:
668 raise
669 account['c7n:password_policy'] = policy
670 if self.match(account['c7n:password_policy']):
671 return resources
672 return []
675@actions.register('set-password-policy')
676class SetAccountPasswordPolicy(BaseAction):
677 """Set an account's password policy.
679 This only changes the policy for the items provided.
680 If this is the first time setting a password policy and an item is not provided it will be
681 set to the defaults defined in the boto docs for IAM.Client.update_account_password_policy
683 :example:
685 .. code-block:: yaml
687 policies:
688 - name: set-account-password-policy
689 resource: account
690 filters:
691 - not:
692 - type: password-policy
693 key: MinimumPasswordLength
694 value: 10
695 op: ge
696 actions:
697 - type: set-password-policy
698 policy:
699 MinimumPasswordLength: 20
700 """
701 schema = type_schema(
702 'set-password-policy',
703 policy={
704 'type': 'object'
705 })
706 shape = 'UpdateAccountPasswordPolicyRequest'
707 service = 'iam'
708 permissions = ('iam:GetAccountPasswordPolicy', 'iam:UpdateAccountPasswordPolicy')
710 def validate(self):
711 return shape_validate(
712 self.data.get('policy', {}),
713 self.shape,
714 self.service)
716 def process(self, resources):
717 client = local_session(self.manager.session_factory).client('iam')
718 account = resources[0]
719 if account.get('c7n:password_policy'):
720 config = account['c7n:password_policy']
721 else:
722 try:
723 config = client.get_account_password_policy().get('PasswordPolicy')
724 except client.exceptions.NoSuchEntityException:
725 config = {}
726 params = dict(self.data['policy'])
727 config.update(params)
728 config = {k: v for (k, v) in config.items() if k not in ('ExpirePasswords',
729 'PasswordPolicyConfigured')}
730 client.update_account_password_policy(**config)
733@filters.register('service-limit')
734class ServiceLimit(Filter):
735 """Check if account's service limits are past a given threshold.
737 Supported limits are per trusted advisor, which is variable based
738 on usage in the account and support level enabled on the account.
740 The `names` attribute lets you filter which checks to query limits
741 about. This is a case-insensitive globbing match on a check name.
742 You can specify a name exactly or use globbing wildcards like `VPC*`.
744 The names are exactly what's shown on the trusted advisor page:
746 https://console.aws.amazon.com/trustedadvisor/home#/category/service-limits
748 or via the awscli:
750 aws --region us-east-1 support describe-trusted-advisor-checks --language en \
751 --query 'checks[?category==`service_limits`].[name]' --output text
753 While you can target individual checks via the `names` attribute, and
754 that should be the preferred method, the following are provided for
755 backward compatibility with the old style of checks:
757 - `services`
759 The resulting limit's `service` field must match one of these.
760 These are case-insensitive globbing matches.
762 Note: If you haven't specified any `names` to filter, then
763 these service names are used as a case-insensitive prefix match on
764 the check name. This helps limit the number of API calls we need
765 to make.
767 - `limits`
769 The resulting limit's `Limit Name` field must match one of these.
770 These are case-insensitive globbing matches.
772 Some example names and their corresponding service and limit names:
774 Check Name Service Limit Name
775 ---------------------------------- -------------- ---------------------------------
776 Auto Scaling Groups AutoScaling Auto Scaling groups
777 Auto Scaling Launch Configurations AutoScaling Launch configurations
778 CloudFormation Stacks CloudFormation Stacks
779 ELB Application Load Balancers ELB Active Application Load Balancers
780 ELB Classic Load Balancers ELB Active load balancers
781 ELB Network Load Balancers ELB Active Network Load Balancers
782 VPC VPC VPCs
783 VPC Elastic IP Address VPC VPC Elastic IP addresses (EIPs)
784 VPC Internet Gateways VPC Internet gateways
786 Note: Some service limits checks are being migrated to service quotas,
787 which is expected to largely replace service limit checks in trusted
788 advisor. In this case, some of these checks have no results.
790 :example:
792 .. code-block:: yaml
794 policies:
795 - name: specific-account-service-limits
796 resource: account
797 filters:
798 - type: service-limit
799 names:
800 - IAM Policies
801 - IAM Roles
802 - "VPC*"
803 threshold: 1.0
805 - name: increase-account-service-limits
806 resource: account
807 filters:
808 - type: service-limit
809 services:
810 - EC2
811 threshold: 1.0
813 - name: specify-region-for-global-service
814 region: us-east-1
815 resource: account
816 filters:
817 - type: service-limit
818 services:
819 - IAM
820 limits:
821 - Roles
822 """
824 schema = type_schema(
825 'service-limit',
826 threshold={'type': 'number'},
827 refresh_period={'type': 'integer',
828 'title': 'how long should a check result be considered fresh'},
829 names={'type': 'array', 'items': {'type': 'string'}},
830 limits={'type': 'array', 'items': {'type': 'string'}},
831 services={'type': 'array', 'items': {
832 'enum': ['AutoScaling', 'CloudFormation',
833 'DynamoDB', 'EBS', 'EC2', 'ELB',
834 'IAM', 'RDS', 'Route53', 'SES', 'VPC']}})
836 permissions = ('support:DescribeTrustedAdvisorCheckRefreshStatuses',
837 'support:DescribeTrustedAdvisorCheckResult',
838 'support:DescribeTrustedAdvisorChecks',
839 'support:RefreshTrustedAdvisorCheck')
840 deprecated_check_ids = ['eW7HH0l7J9']
841 check_limit = ('region', 'service', 'check', 'limit', 'extant', 'color')
843 # When doing a refresh, how long to wait for the check to become ready.
844 # Max wait here is 5 * 10 ~ 50 seconds.
845 poll_interval = 5
846 poll_max_intervals = 10
847 global_services = {'IAM'}
849 def validate(self):
850 region = self.manager.data.get('region', '')
851 if len(self.global_services.intersection(self.data.get('services', []))):
852 if region != 'us-east-1':
853 raise PolicyValidationError(
854 "Global services: %s must be targeted in us-east-1 on the policy"
855 % ', '.join(self.global_services))
856 return self
858 @classmethod
859 def get_check_result(cls, client, check_id):
860 checks = client.describe_trusted_advisor_check_result(
861 checkId=check_id, language='en')['result']
863 # Check status and if necessary refresh checks
864 if checks['status'] == 'not_available':
865 try:
866 client.refresh_trusted_advisor_check(checkId=check_id)
867 except ClientError as e:
868 if e.response['Error']['Code'] == 'InvalidParameterValueException':
869 cls.log.warning("InvalidParameterValueException: %s",
870 e.response['Error']['Message'])
871 return
873 for _ in range(cls.poll_max_intervals):
874 time.sleep(cls.poll_interval)
875 refresh_response = client.describe_trusted_advisor_check_refresh_statuses(
876 checkIds=[check_id])
877 if refresh_response['statuses'][0]['status'] == 'success':
878 checks = client.describe_trusted_advisor_check_result(
879 checkId=check_id, language='en')['result']
880 break
881 return checks
883 def get_available_checks(self, client, category='service_limits'):
884 checks = client.describe_trusted_advisor_checks(language='en')
885 return [c for c in checks['checks']
886 if c['category'] == category and
887 c['id'] not in self.deprecated_check_ids]
889 def match_patterns_to_value(self, patterns, value):
890 for p in patterns:
891 if fnmatch(value.lower(), p.lower()):
892 return True
893 return False
895 def should_process(self, name):
896 # if names specified, limit to these names
897 patterns = self.data.get('names')
898 if patterns:
899 return self.match_patterns_to_value(patterns, name)
901 # otherwise, if services specified, limit to those prefixes
902 services = self.data.get('services')
903 if services:
904 patterns = ["{}*".format(i) for i in services]
905 return self.match_patterns_to_value(patterns, name.replace(' ', ''))
907 return True
909 def process(self, resources, event=None):
910 support_region = get_support_region(self.manager)
911 client = local_session(self.manager.session_factory).client(
912 'support', region_name=support_region)
913 checks = self.get_available_checks(client)
914 exceeded = []
915 for check in checks:
916 if not self.should_process(check['name']):
917 continue
918 matched = self.process_check(client, check, resources, event)
919 if matched:
920 for m in matched:
921 m['check_id'] = check['id']
922 m['name'] = check['name']
923 exceeded.extend(matched)
924 if exceeded:
925 resources[0]['c7n:ServiceLimitsExceeded'] = exceeded
926 return resources
927 return []
929 def process_check(self, client, check, resources, event=None):
930 region = self.manager.config.region
931 results = self.get_check_result(client, check['id'])
933 if results is None or 'flaggedResources' not in results:
934 return []
936 # trim to only results for this region
937 results['flaggedResources'] = [
938 r
939 for r in results.get('flaggedResources', [])
940 if r['metadata'][0] == region or (r['metadata'][0] == '-' and region == 'us-east-1')
941 ]
943 # save all raw limit results to the account resource
944 if 'c7n:ServiceLimits' not in resources[0]:
945 resources[0]['c7n:ServiceLimits'] = []
946 resources[0]['c7n:ServiceLimits'].append(results)
948 # check if we need to refresh the check for next time
949 delta = datetime.timedelta(self.data.get('refresh_period', 1))
950 check_date = parse_date(results['timestamp'])
951 if datetime.datetime.now(tz=tzutc()) - delta > check_date:
952 try:
953 client.refresh_trusted_advisor_check(checkId=check['id'])
954 except ClientError as e:
955 if e.response['Error']['Code'] == 'InvalidParameterValueException':
956 self.log.warning("InvalidParameterValueException: %s",
957 e.response['Error']['Message'])
958 return
960 services = self.data.get('services')
961 limits = self.data.get('limits')
962 threshold = self.data.get('threshold')
963 exceeded = []
965 for resource in results['flaggedResources']:
966 if threshold is None and resource['status'] == 'ok':
967 continue
968 limit = dict(zip(self.check_limit, resource['metadata']))
969 if services and not self.match_patterns_to_value(services, limit['service']):
970 continue
971 if limits and not self.match_patterns_to_value(limits, limit['check']):
972 continue
973 limit['status'] = resource['status']
974 limit['percentage'] = (
975 float(limit['extant'] or 0) / float(limit['limit']) * 100
976 )
977 if threshold and limit['percentage'] < threshold:
978 continue
979 exceeded.append(limit)
980 return exceeded
983@actions.register('request-limit-increase')
984class RequestLimitIncrease(BaseAction):
985 r"""File support ticket to raise limit.
987 :Example:
989 .. code-block:: yaml
991 policies:
992 - name: raise-account-service-limits
993 resource: account
994 filters:
995 - type: service-limit
996 services:
997 - EBS
998 limits:
999 - Provisioned IOPS (SSD) storage (GiB)
1000 threshold: 60.5
1001 actions:
1002 - type: request-limit-increase
1003 notify: [email, email2]
1004 ## You can use one of either percent-increase or an amount-increase.
1005 percent-increase: 50
1006 message: "Please raise the below account limit(s); \n {limits}"
1007 """
1009 schema = {
1010 'type': 'object',
1011 'additionalProperties': False,
1012 'properties': {
1013 'type': {'enum': ['request-limit-increase']},
1014 'percent-increase': {'type': 'number', 'minimum': 1},
1015 'amount-increase': {'type': 'number', 'minimum': 1},
1016 'minimum-increase': {'type': 'number', 'minimum': 1},
1017 'subject': {'type': 'string'},
1018 'message': {'type': 'string'},
1019 'notify': {'type': 'array', 'items': {'type': 'string'}},
1020 'severity': {'type': 'string', 'enum': ['urgent', 'high', 'normal', 'low']}
1021 },
1022 'oneOf': [
1023 {'required': ['type', 'percent-increase']},
1024 {'required': ['type', 'amount-increase']}
1025 ]
1026 }
1028 permissions = ('support:CreateCase',)
1030 default_subject = '[Account:{account}]Raise the following limit(s) of {service} in {region}'
1031 default_template = 'Please raise the below account limit(s); \n {limits}'
1032 default_severity = 'normal'
1034 service_code_mapping = {
1035 'AutoScaling': 'auto-scaling',
1036 'CloudFormation': 'aws-cloudformation',
1037 'DynamoDB': 'amazon-dynamodb',
1038 'EBS': 'amazon-elastic-block-store',
1039 'EC2': 'amazon-elastic-compute-cloud-linux',
1040 'ELB': 'elastic-load-balancing',
1041 'IAM': 'aws-identity-and-access-management',
1042 'Kinesis': 'amazon-kinesis',
1043 'RDS': 'amazon-relational-database-service-aurora',
1044 'Route53': 'amazon-route53',
1045 'SES': 'amazon-simple-email-service',
1046 'VPC': 'amazon-virtual-private-cloud',
1047 }
1049 def process(self, resources):
1050 support_region = get_support_region(self.manager)
1051 client = local_session(self.manager.session_factory).client(
1052 'support', region_name=support_region)
1053 account_id = self.manager.config.account_id
1054 service_map = {}
1055 region_map = {}
1056 limit_exceeded = resources[0].get('c7n:ServiceLimitsExceeded', [])
1057 percent_increase = self.data.get('percent-increase')
1058 amount_increase = self.data.get('amount-increase')
1059 minimum_increase = self.data.get('minimum-increase', 1)
1061 for s in limit_exceeded:
1062 current_limit = int(s['limit'])
1063 if percent_increase:
1064 increase_by = current_limit * float(percent_increase) / 100
1065 increase_by = max(increase_by, minimum_increase)
1066 else:
1067 increase_by = amount_increase
1068 increase_by = round(increase_by)
1069 msg = '\nIncrease %s by %d in %s \n\t Current Limit: %s\n\t Current Usage: %s\n\t ' \
1070 'Set New Limit to: %d' % (
1071 s['check'], increase_by, s['region'], s['limit'], s['extant'],
1072 (current_limit + increase_by))
1073 service_map.setdefault(s['service'], []).append(msg)
1074 region_map.setdefault(s['service'], s['region'])
1076 for service in service_map:
1077 subject = self.data.get('subject', self.default_subject).format(
1078 service=service, region=region_map[service], account=account_id)
1079 service_code = self.service_code_mapping.get(service)
1080 body = self.data.get('message', self.default_template)
1081 body = body.format(**{
1082 'service': service,
1083 'limits': '\n\t'.join(service_map[service]),
1084 })
1085 client.create_case(
1086 subject=subject,
1087 communicationBody=body,
1088 serviceCode=service_code,
1089 categoryCode='general-guidance',
1090 severityCode=self.data.get('severity', self.default_severity),
1091 ccEmailAddresses=self.data.get('notify', []))
1094def cloudtrail_policy(original, bucket_name, account_id, bucket_region):
1095 '''add CloudTrail permissions to an S3 policy, preserving existing'''
1096 ct_actions = [
1097 {
1098 'Action': 's3:GetBucketAcl',
1099 'Effect': 'Allow',
1100 'Principal': {'Service': 'cloudtrail.amazonaws.com'},
1101 'Resource': generate_arn(
1102 service='s3', resource=bucket_name, region=bucket_region),
1103 'Sid': 'AWSCloudTrailAclCheck20150319',
1104 },
1105 {
1106 'Action': 's3:PutObject',
1107 'Condition': {
1108 'StringEquals':
1109 {'s3:x-amz-acl': 'bucket-owner-full-control'},
1110 },
1111 'Effect': 'Allow',
1112 'Principal': {'Service': 'cloudtrail.amazonaws.com'},
1113 'Resource': generate_arn(
1114 service='s3', resource=bucket_name, region=bucket_region),
1115 'Sid': 'AWSCloudTrailWrite20150319',
1116 },
1117 ]
1118 # parse original policy
1119 if original is None:
1120 policy = {
1121 'Statement': [],
1122 'Version': '2012-10-17',
1123 }
1124 else:
1125 policy = json.loads(original['Policy'])
1126 original_actions = [a.get('Action') for a in policy['Statement']]
1127 for cta in ct_actions:
1128 if cta['Action'] not in original_actions:
1129 policy['Statement'].append(cta)
1130 return json.dumps(policy)
1133# AWS Account doesn't participate in events (not based on query resource manager)
1134# so the event subscriber used by postfinding to register doesn't apply, manually
1135# register it.
1136Account.action_registry.register('post-finding', OtherResourcePostFinding)
1139@actions.register('enable-cloudtrail')
1140class EnableTrail(BaseAction):
1141 """Enables logging on the trail(s) named in the policy
1143 :Example:
1145 .. code-block:: yaml
1147 policies:
1148 - name: trail-test
1149 description: Ensure CloudTrail logging is enabled
1150 resource: account
1151 actions:
1152 - type: enable-cloudtrail
1153 trail: mytrail
1154 bucket: trails
1155 """
1157 permissions = (
1158 'cloudtrail:CreateTrail',
1159 'cloudtrail:DescribeTrails',
1160 'cloudtrail:GetTrailStatus',
1161 'cloudtrail:StartLogging',
1162 'cloudtrail:UpdateTrail',
1163 's3:CreateBucket',
1164 's3:GetBucketPolicy',
1165 's3:PutBucketPolicy',
1166 )
1167 schema = type_schema(
1168 'enable-cloudtrail',
1169 **{
1170 'trail': {'type': 'string'},
1171 'bucket': {'type': 'string'},
1172 'bucket-region': {'type': 'string'},
1173 'multi-region': {'type': 'boolean'},
1174 'global-events': {'type': 'boolean'},
1175 'notify': {'type': 'string'},
1176 'file-digest': {'type': 'boolean'},
1177 'kms': {'type': 'boolean'},
1178 'kms-key': {'type': 'string'},
1179 'required': ('bucket',),
1180 }
1181 )
1183 def process(self, accounts):
1184 """Create or enable CloudTrail"""
1185 session = local_session(self.manager.session_factory)
1186 client = session.client('cloudtrail')
1187 bucket_name = self.data['bucket']
1188 bucket_region = self.data.get('bucket-region', 'us-east-1')
1189 trail_name = self.data.get('trail', 'default-trail')
1190 multi_region = self.data.get('multi-region', True)
1191 global_events = self.data.get('global-events', True)
1192 notify = self.data.get('notify', '')
1193 file_digest = self.data.get('file-digest', False)
1194 kms = self.data.get('kms', False)
1195 kms_key = self.data.get('kms-key', '')
1197 s3client = session.client('s3', region_name=bucket_region)
1198 try:
1199 s3client.create_bucket(
1200 Bucket=bucket_name,
1201 CreateBucketConfiguration={'LocationConstraint': bucket_region}
1202 )
1203 except ClientError as ce:
1204 if not ('Error' in ce.response and
1205 ce.response['Error']['Code'] == 'BucketAlreadyOwnedByYou'):
1206 raise ce
1208 try:
1209 current_policy = s3client.get_bucket_policy(Bucket=bucket_name)
1210 except ClientError:
1211 current_policy = None
1213 policy_json = cloudtrail_policy(
1214 current_policy, bucket_name,
1215 self.manager.config.account_id, bucket_region)
1217 s3client.put_bucket_policy(Bucket=bucket_name, Policy=policy_json)
1218 trails = client.describe_trails().get('trailList', ())
1219 if trail_name not in [t.get('Name') for t in trails]:
1220 new_trail = client.create_trail(
1221 Name=trail_name,
1222 S3BucketName=bucket_name,
1223 )
1224 if new_trail:
1225 trails.append(new_trail)
1226 # the loop below will configure the new trail
1227 for trail in trails:
1228 if trail.get('Name') != trail_name:
1229 continue
1230 # enable
1231 arn = trail['TrailARN']
1232 status = client.get_trail_status(Name=arn)
1233 if not status['IsLogging']:
1234 client.start_logging(Name=arn)
1235 # apply configuration changes (if any)
1236 update_args = {}
1237 if multi_region != trail.get('IsMultiRegionTrail'):
1238 update_args['IsMultiRegionTrail'] = multi_region
1239 if global_events != trail.get('IncludeGlobalServiceEvents'):
1240 update_args['IncludeGlobalServiceEvents'] = global_events
1241 if notify != trail.get('SNSTopicArn'):
1242 update_args['SnsTopicName'] = notify
1243 if file_digest != trail.get('LogFileValidationEnabled'):
1244 update_args['EnableLogFileValidation'] = file_digest
1245 if kms_key != trail.get('KmsKeyId'):
1246 if not kms and 'KmsKeyId' in trail:
1247 kms_key = ''
1248 update_args['KmsKeyId'] = kms_key
1249 if update_args:
1250 update_args['Name'] = trail_name
1251 client.update_trail(**update_args)
1254@filters.register('has-virtual-mfa')
1255class HasVirtualMFA(Filter):
1256 """Is the account configured with a virtual MFA device?
1258 :example:
1260 .. code-block:: yaml
1262 policies:
1263 - name: account-with-virtual-mfa
1264 resource: account
1265 region: us-east-1
1266 filters:
1267 - type: has-virtual-mfa
1268 value: true
1269 """
1271 schema = type_schema('has-virtual-mfa', **{'value': {'type': 'boolean'}})
1273 permissions = ('iam:ListVirtualMFADevices',)
1275 def mfa_belongs_to_root_account(self, mfa):
1276 return mfa['SerialNumber'].endswith(':mfa/root-account-mfa-device')
1278 def account_has_virtual_mfa(self, account):
1279 if not account.get('c7n:VirtualMFADevices'):
1280 client = local_session(self.manager.session_factory).client('iam')
1281 paginator = client.get_paginator('list_virtual_mfa_devices')
1282 raw_list = paginator.paginate().build_full_result()['VirtualMFADevices']
1283 account['c7n:VirtualMFADevices'] = list(filter(
1284 self.mfa_belongs_to_root_account, raw_list))
1285 expect_virtual_mfa = self.data.get('value', True)
1286 has_virtual_mfa = any(account['c7n:VirtualMFADevices'])
1287 return expect_virtual_mfa == has_virtual_mfa
1289 def process(self, resources, event=None):
1290 return list(filter(self.account_has_virtual_mfa, resources))
1293@actions.register('enable-data-events')
1294class EnableDataEvents(BaseAction):
1295 """Ensure all buckets in account are setup to log data events.
1297 Note this works via a single trail for data events per
1298 https://aws.amazon.com/about-aws/whats-new/2017/09/aws-cloudtrail-enables-option-to-add-all-amazon-s3-buckets-to-data-events/
1300 This trail should NOT be used for api management events, the
1301 configuration here is soley for data events. If directed to create
1302 a trail this will do so without management events.
1304 :example:
1306 .. code-block:: yaml
1308 policies:
1309 - name: s3-enable-data-events-logging
1310 resource: account
1311 actions:
1312 - type: enable-data-events
1313 data-trail:
1314 name: s3-events
1315 multi-region: us-east-1
1316 """
1318 schema = type_schema(
1319 'enable-data-events', required=['data-trail'], **{
1320 'data-trail': {
1321 'type': 'object',
1322 'additionalProperties': False,
1323 'required': ['name'],
1324 'properties': {
1325 'create': {
1326 'title': 'Should we create trail if needed for events?',
1327 'type': 'boolean'},
1328 'type': {'enum': ['ReadOnly', 'WriteOnly', 'All']},
1329 'name': {
1330 'title': 'The name of the event trail',
1331 'type': 'string'},
1332 'topic': {
1333 'title': 'If creating, the sns topic for the trail to send updates',
1334 'type': 'string'},
1335 's3-bucket': {
1336 'title': 'If creating, the bucket to store trail event data',
1337 'type': 'string'},
1338 's3-prefix': {'type': 'string'},
1339 'key-id': {
1340 'title': 'If creating, Enable kms on the trail',
1341 'type': 'string'},
1342 # region that we're aggregating via trails.
1343 'multi-region': {
1344 'title': 'If creating, use this region for all data trails',
1345 'type': 'string'}}}})
1347 def validate(self):
1348 if self.data['data-trail'].get('create'):
1349 if 's3-bucket' not in self.data['data-trail']:
1350 raise PolicyValidationError(
1351 "If creating data trails, an s3-bucket is required on %s" % (
1352 self.manager.data))
1353 return self
1355 def get_permissions(self):
1356 perms = [
1357 'cloudtrail:DescribeTrails',
1358 'cloudtrail:GetEventSelectors',
1359 'cloudtrail:PutEventSelectors']
1361 if self.data.get('data-trail', {}).get('create'):
1362 perms.extend([
1363 'cloudtrail:CreateTrail', 'cloudtrail:StartLogging'])
1364 return perms
1366 def add_data_trail(self, client, trail_cfg):
1367 if not trail_cfg.get('create'):
1368 raise ValueError(
1369 "s3 data event trail missing and not configured to create")
1370 params = dict(
1371 Name=trail_cfg['name'],
1372 S3BucketName=trail_cfg['s3-bucket'],
1373 EnableLogFileValidation=True)
1375 if 'key-id' in trail_cfg:
1376 params['KmsKeyId'] = trail_cfg['key-id']
1377 if 's3-prefix' in trail_cfg:
1378 params['S3KeyPrefix'] = trail_cfg['s3-prefix']
1379 if 'topic' in trail_cfg:
1380 params['SnsTopicName'] = trail_cfg['topic']
1381 if 'multi-region' in trail_cfg:
1382 params['IsMultiRegionTrail'] = True
1384 client.create_trail(**params)
1385 return {'Name': trail_cfg['name']}
1387 def process(self, resources):
1388 session = local_session(self.manager.session_factory)
1389 region = self.data['data-trail'].get('multi-region')
1391 if region:
1392 client = session.client('cloudtrail', region_name=region)
1393 else:
1394 client = session.client('cloudtrail')
1396 added = False
1397 tconfig = self.data['data-trail']
1398 trails = client.describe_trails(
1399 trailNameList=[tconfig['name']]).get('trailList', ())
1400 if not trails:
1401 trail = self.add_data_trail(client, tconfig)
1402 added = True
1403 else:
1404 trail = trails[0]
1406 events = client.get_event_selectors(
1407 TrailName=trail['Name']).get('EventSelectors', [])
1409 for e in events:
1410 found = False
1411 if not e.get('DataResources'):
1412 continue
1413 for data_events in e['DataResources']:
1414 if data_events['Type'] != 'AWS::S3::Object':
1415 continue
1416 for b in data_events['Values']:
1417 if b.rsplit(':')[-1].strip('/') == '':
1418 found = True
1419 break
1420 if found:
1421 resources[0]['c7n_data_trail'] = trail
1422 return
1424 # Opinionated choice, separate api and data events.
1425 event_count = len(events)
1426 events = [e for e in events if not e.get('IncludeManagementEvents')]
1427 if len(events) != event_count:
1428 self.log.warning("removing api trail from data trail")
1430 # future proof'd for other data events, for s3 this trail
1431 # encompasses all the buckets in the account.
1433 events.append({
1434 'IncludeManagementEvents': False,
1435 'ReadWriteType': tconfig.get('type', 'All'),
1436 'DataResources': [{
1437 'Type': 'AWS::S3::Object',
1438 'Values': ['arn:aws:s3:::']}]})
1439 client.put_event_selectors(
1440 TrailName=trail['Name'],
1441 EventSelectors=events)
1443 if added:
1444 client.start_logging(Name=tconfig['name'])
1446 resources[0]['c7n_data_trail'] = trail
1449@filters.register('shield-enabled')
1450class ShieldEnabled(Filter):
1452 permissions = ('shield:DescribeSubscription',)
1454 schema = type_schema(
1455 'shield-enabled',
1456 state={'type': 'boolean'})
1458 def process(self, resources, event=None):
1459 state = self.data.get('state', False)
1460 client = local_session(self.manager.session_factory).client('shield')
1461 try:
1462 subscription = client.describe_subscription().get(
1463 'Subscription', None)
1464 except ClientError as e:
1465 if e.response['Error']['Code'] != 'ResourceNotFoundException':
1466 raise
1467 subscription = None
1469 resources[0]['c7n:ShieldSubscription'] = subscription
1470 if state and subscription:
1471 return resources
1472 elif not state and not subscription:
1473 return resources
1474 return []
1477@actions.register('set-shield-advanced')
1478class SetShieldAdvanced(BaseAction):
1479 """Enable/disable Shield Advanced on an account."""
1481 permissions = (
1482 'shield:CreateSubscription', 'shield:DeleteSubscription')
1484 schema = type_schema(
1485 'set-shield-advanced',
1486 state={'type': 'boolean'})
1488 def process(self, resources):
1489 client = local_session(self.manager.session_factory).client('shield')
1490 state = self.data.get('state', True)
1492 if state:
1493 client.create_subscription()
1494 else:
1495 try:
1496 client.delete_subscription()
1497 except ClientError as e:
1498 if e.response['Error']['Code'] == 'ResourceNotFoundException':
1499 return
1500 raise
1503@filters.register('xray-encrypt-key')
1504class XrayEncrypted(Filter):
1505 """Determine if xray is encrypted.
1507 :example:
1509 .. code-block:: yaml
1511 policies:
1512 - name: xray-encrypt-with-default
1513 resource: aws.account
1514 filters:
1515 - type: xray-encrypt-key
1516 key: default
1517 - name: xray-encrypt-with-kms
1518 resource: aws.account
1519 filters:
1520 - type: xray-encrypt-key
1521 key: kms
1522 - name: xray-encrypt-with-specific-key
1523 resource: aws.account
1524 filters:
1525 - type: xray-encrypt-key
1526 key: alias/my-alias or arn or keyid
1527 """
1529 permissions = ('xray:GetEncryptionConfig',)
1530 schema = type_schema(
1531 'xray-encrypt-key',
1532 required=['key'],
1533 key={'type': 'string'}
1534 )
1536 def process(self, resources, event=None):
1537 client = self.manager.session_factory().client('xray')
1538 gec_result = client.get_encryption_config()['EncryptionConfig']
1539 resources[0]['c7n:XrayEncryptionConfig'] = gec_result
1541 k = self.data.get('key')
1542 if k not in ['default', 'kms']:
1543 kmsclient = self.manager.session_factory().client('kms')
1544 keyid = kmsclient.describe_key(KeyId=k)['KeyMetadata']['Arn']
1545 rc = resources if (gec_result['KeyId'] == keyid) else []
1546 else:
1547 kv = 'KMS' if self.data.get('key') == 'kms' else 'NONE'
1548 rc = resources if (gec_result['Type'] == kv) else []
1549 return rc
1552@actions.register('set-xray-encrypt')
1553class SetXrayEncryption(BaseAction):
1554 """Enable specific xray encryption.
1556 :example:
1558 .. code-block:: yaml
1560 policies:
1561 - name: xray-default-encrypt
1562 resource: aws.account
1563 actions:
1564 - type: set-xray-encrypt
1565 key: default
1566 - name: xray-kms-encrypt
1567 resource: aws.account
1568 actions:
1569 - type: set-xray-encrypt
1570 key: alias/some/alias/key
1571 """
1573 permissions = ('xray:PutEncryptionConfig',)
1574 schema = type_schema(
1575 'set-xray-encrypt',
1576 required=['key'],
1577 key={'type': 'string'}
1578 )
1580 def process(self, resources):
1581 client = local_session(self.manager.session_factory).client('xray')
1582 key = self.data.get('key')
1583 req = {'Type': 'NONE'} if key == 'default' else {'Type': 'KMS', 'KeyId': key}
1584 client.put_encryption_config(**req)
1587@filters.register('default-ebs-encryption')
1588class EbsEncryption(Filter):
1589 """Filter an account by its ebs encryption status.
1591 By default for key we match on the alias name for a key.
1593 :example:
1595 .. code-block:: yaml
1597 policies:
1598 - name: check-default-ebs-encryption
1599 resource: aws.account
1600 filters:
1601 - type: default-ebs-encryption
1602 key: "alias/aws/ebs"
1603 state: true
1605 It is also possible to match on specific key attributes (tags, origin)
1607 :example:
1609 .. code-block:: yaml
1611 policies:
1612 - name: check-ebs-encryption-key-origin
1613 resource: aws.account
1614 filters:
1615 - type: default-ebs-encryption
1616 key:
1617 type: value
1618 key: Origin
1619 value: AWS_KMS
1620 state: true
1621 """
1622 permissions = ('ec2:GetEbsEncryptionByDefault',)
1623 schema = type_schema(
1624 'default-ebs-encryption',
1625 state={'type': 'boolean'},
1626 key={'oneOf': [
1627 {'$ref': '#/definitions/filters/value'},
1628 {'type': 'string'}]})
1630 def process(self, resources, event=None):
1631 state = self.data.get('state', False)
1632 client = local_session(self.manager.session_factory).client('ec2')
1633 account_state = client.get_ebs_encryption_by_default().get(
1634 'EbsEncryptionByDefault')
1635 if account_state != state:
1636 return []
1637 if state and 'key' in self.data:
1638 vfd = (isinstance(self.data['key'], dict) and
1639 self.data['key'] or {'c7n:AliasName': self.data['key']})
1640 vf = KmsRelatedFilter(vfd, self.manager)
1641 vf.RelatedIdsExpression = 'KmsKeyId'
1642 vf.annotate = False
1643 key = client.get_ebs_default_kms_key_id().get('KmsKeyId')
1644 if not vf.process([{'KmsKeyId': key}]):
1645 return []
1646 return resources
1649@actions.register('set-ebs-encryption')
1650class SetEbsEncryption(BaseAction):
1651 """Set AWS EBS default encryption on an account
1653 :example:
1655 .. code-block:: yaml
1657 policies:
1658 - name: set-default-ebs-encryption
1659 resource: aws.account
1660 filters:
1661 - type: default-ebs-encryption
1662 state: false
1663 actions:
1664 - type: set-ebs-encryption
1665 state: true
1666 key: alias/aws/ebs
1667 """
1668 permissions = ('ec2:EnableEbsEncryptionByDefault',
1669 'ec2:DisableEbsEncryptionByDefault')
1671 schema = type_schema(
1672 'set-ebs-encryption',
1673 state={'type': 'boolean'},
1674 key={'type': 'string'})
1676 def process(self, resources):
1677 client = local_session(
1678 self.manager.session_factory).client('ec2')
1679 state = self.data.get('state')
1680 key = self.data.get('key')
1681 if state:
1682 client.enable_ebs_encryption_by_default()
1683 else:
1684 client.disable_ebs_encryption_by_default()
1686 if state and key:
1687 client.modify_ebs_default_kms_key_id(
1688 KmsKeyId=self.data['key'])
1691@filters.register('s3-public-block')
1692class S3PublicBlock(ValueFilter):
1693 """Check for s3 public blocks on an account.
1695 https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html
1696 """
1698 annotation_key = 'c7n:s3-public-block'
1699 annotate = False # no annotation from value filter
1700 schema = type_schema('s3-public-block', rinherit=ValueFilter.schema)
1701 schema_alias = False
1702 permissions = ('s3:GetAccountPublicAccessBlock',)
1704 def process(self, resources, event=None):
1705 self.augment([r for r in resources if self.annotation_key not in r])
1706 return super(S3PublicBlock, self).process(resources, event)
1708 def augment(self, resources):
1709 client = local_session(self.manager.session_factory).client('s3control')
1710 for r in resources:
1711 try:
1712 r[self.annotation_key] = client.get_public_access_block(
1713 AccountId=r['account_id']).get('PublicAccessBlockConfiguration', {})
1714 except client.exceptions.NoSuchPublicAccessBlockConfiguration:
1715 r[self.annotation_key] = {}
1717 def __call__(self, r):
1718 return super(S3PublicBlock, self).__call__(r[self.annotation_key])
1721@actions.register('set-s3-public-block')
1722class SetS3PublicBlock(BaseAction):
1723 """Configure S3 Public Access Block on an account.
1725 All public access block attributes can be set. If not specified they are merged
1726 with the extant configuration.
1728 https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html
1730 :example:
1732 .. yaml:
1734 policies:
1735 - name: restrict-public-buckets
1736 resource: aws.account
1737 filters:
1738 - not:
1739 - type: s3-public-block
1740 key: RestrictPublicBuckets
1741 value: true
1742 actions:
1743 - type: set-s3-public-block
1744 RestrictPublicBuckets: true
1746 """
1747 schema = type_schema(
1748 'set-s3-public-block',
1749 state={'type': 'boolean', 'default': True},
1750 BlockPublicAcls={'type': 'boolean'},
1751 IgnorePublicAcls={'type': 'boolean'},
1752 BlockPublicPolicy={'type': 'boolean'},
1753 RestrictPublicBuckets={'type': 'boolean'})
1755 permissions = ('s3:PutAccountPublicAccessBlock', 's3:GetAccountPublicAccessBlock')
1757 def validate(self):
1758 config = self.data.copy()
1759 config.pop('type')
1760 if config.pop('state', None) is False and config:
1761 raise PolicyValidationError(
1762 "{} cant set state false with controls specified".format(
1763 self.type))
1765 def process(self, resources):
1766 client = local_session(self.manager.session_factory).client('s3control')
1767 if self.data.get('state', True) is False:
1768 for r in resources:
1769 client.delete_public_access_block(AccountId=r['account_id'])
1770 return
1772 keys = (
1773 'BlockPublicPolicy', 'BlockPublicAcls', 'IgnorePublicAcls', 'RestrictPublicBuckets')
1775 for r in resources:
1776 # try to merge with existing configuration if not explicitly set.
1777 base = {}
1778 if S3PublicBlock.annotation_key in r:
1779 base = r[S3PublicBlock.annotation_key]
1780 else:
1781 try:
1782 base = client.get_public_access_block(AccountId=r['account_id']).get(
1783 'PublicAccessBlockConfiguration')
1784 except client.exceptions.NoSuchPublicAccessBlockConfiguration:
1785 base = {}
1787 config = {}
1788 for k in keys:
1789 if k in self.data:
1790 config[k] = self.data[k]
1791 elif k in base:
1792 config[k] = base[k]
1794 client.put_public_access_block(
1795 AccountId=r['account_id'],
1796 PublicAccessBlockConfiguration=config)
1799class GlueCatalogEncryptionEnabled(MultiAttrFilter):
1800 """ Filter glue catalog by its glue encryption status and KMS key
1802 :example:
1804 .. code-block:: yaml
1806 policies:
1807 - name: glue-catalog-security-config
1808 resource: aws.glue-catalog
1809 filters:
1810 - type: glue-security-config
1811 SseAwsKmsKeyId: alias/aws/glue
1813 """
1814 retry = staticmethod(QueryResourceManager.retry)
1816 schema = {
1817 'type': 'object',
1818 'additionalProperties': False,
1819 'properties': {
1820 'type': {'enum': ['glue-security-config']},
1821 'CatalogEncryptionMode': {'enum': ['DISABLED', 'SSE-KMS']},
1822 'SseAwsKmsKeyId': {'type': 'string'},
1823 'ReturnConnectionPasswordEncrypted': {'type': 'boolean'},
1824 'AwsKmsKeyId': {'type': 'string'}
1825 }
1826 }
1828 annotation = "c7n:glue-security-config"
1829 permissions = ('glue:GetDataCatalogEncryptionSettings',)
1831 def validate(self):
1832 attrs = set()
1833 for key in self.data:
1834 if key in ['CatalogEncryptionMode',
1835 'ReturnConnectionPasswordEncrypted',
1836 'SseAwsKmsKeyId',
1837 'AwsKmsKeyId']:
1838 attrs.add(key)
1839 self.multi_attrs = attrs
1840 return super(GlueCatalogEncryptionEnabled, self).validate()
1842 def get_target(self, resource):
1843 if self.annotation in resource:
1844 return resource[self.annotation]
1845 client = local_session(self.manager.session_factory).client('glue')
1846 encryption_setting = resource.get('DataCatalogEncryptionSettings')
1847 if self.manager.type != 'glue-catalog':
1848 encryption_setting = client.get_data_catalog_encryption_settings().get(
1849 'DataCatalogEncryptionSettings')
1850 resource[self.annotation] = encryption_setting.get('EncryptionAtRest')
1851 resource[self.annotation].update(encryption_setting.get('ConnectionPasswordEncryption'))
1852 key_attrs = ('SseAwsKmsKeyId', 'AwsKmsKeyId')
1853 for encrypt_attr in key_attrs:
1854 if encrypt_attr not in self.data or not self.data[encrypt_attr].startswith('alias'):
1855 continue
1856 key = resource[self.annotation].get(encrypt_attr)
1857 vfd = {'c7n:AliasName': self.data[encrypt_attr]}
1858 vf = KmsRelatedFilter(vfd, self.manager)
1859 vf.RelatedIdsExpression = 'KmsKeyId'
1860 vf.annotate = False
1861 if not vf.process([{'KmsKeyId': key}]):
1862 return []
1863 resource[self.annotation][encrypt_attr] = self.data[encrypt_attr]
1864 return resource[self.annotation]
1867@filters.register('glue-security-config')
1868class AccountCatalogEncryptionFilter(GlueCatalogEncryptionEnabled):
1869 """Filter aws account by its glue encryption status and KMS key
1871 :example:
1873 .. code-block:: yaml
1875 policies:
1876 - name: glue-security-config
1877 resource: aws.account
1878 filters:
1879 - type: glue-security-config
1880 SseAwsKmsKeyId: alias/aws/glue
1882 """
1885@filters.register('emr-block-public-access')
1886class EMRBlockPublicAccessConfiguration(ValueFilter):
1887 """Check for EMR block public access configuration on an account
1889 :example:
1891 .. code-block:: yaml
1893 policies:
1894 - name: get-emr-block-public-access
1895 resource: account
1896 filters:
1897 - type: emr-block-public-access
1898 """
1900 annotation_key = 'c7n:emr-block-public-access'
1901 annotate = False # no annotation from value filter
1902 schema = type_schema('emr-block-public-access', rinherit=ValueFilter.schema)
1903 schema_alias = False
1904 permissions = ("elasticmapreduce:GetBlockPublicAccessConfiguration",)
1906 def process(self, resources, event=None):
1907 self.augment([r for r in resources if self.annotation_key not in r])
1908 return super().process(resources, event)
1910 def augment(self, resources):
1911 client = local_session(self.manager.session_factory).client(
1912 'emr', region_name=self.manager.config.region)
1914 for r in resources:
1915 try:
1916 r[self.annotation_key] = client.get_block_public_access_configuration()
1917 r[self.annotation_key].pop('ResponseMetadata')
1918 except client.exceptions.NoSuchPublicAccessBlockConfiguration:
1919 r[self.annotation_key] = {}
1921 def __call__(self, r):
1922 return super(EMRBlockPublicAccessConfiguration, self).__call__(r[self.annotation_key])
1925@actions.register('set-emr-block-public-access')
1926class PutAccountBlockPublicAccessConfiguration(BaseAction):
1927 """Action to put/update the EMR block public access configuration for your
1928 AWS account in the current region
1930 :example:
1932 .. code-block:: yaml
1934 policies:
1935 - name: set-emr-block-public-access
1936 resource: account
1937 filters:
1938 - type: emr-block-public-access
1939 key: BlockPublicAccessConfiguration.BlockPublicSecurityGroupRules
1940 value: False
1941 actions:
1942 - type: set-emr-block-public-access
1943 config:
1944 BlockPublicSecurityGroupRules: True
1945 PermittedPublicSecurityGroupRuleRanges:
1946 - MinRange: 22
1947 MaxRange: 22
1948 - MinRange: 23
1949 MaxRange: 23
1951 """
1953 schema = type_schema('set-emr-block-public-access',
1954 config={"type": "object",
1955 'properties': {
1956 'BlockPublicSecurityGroupRules': {'type': 'boolean'},
1957 'PermittedPublicSecurityGroupRuleRanges': {
1958 'type': 'array',
1959 'items': {
1960 'type': 'object',
1961 'properties': {
1962 'MinRange': {'type': 'number', "minimum": 0},
1963 'MaxRange': {'type': 'number', "minimum": 0}
1964 },
1965 'required': ['MinRange']
1966 }
1967 }
1968 },
1969 'required': ['BlockPublicSecurityGroupRules']
1970 },
1971 required=('config',))
1973 permissions = ("elasticmapreduce:PutBlockPublicAccessConfiguration",)
1975 def process(self, resources):
1976 client = local_session(self.manager.session_factory).client('emr')
1977 r = resources[0]
1979 base = {}
1980 if EMRBlockPublicAccessConfiguration.annotation_key in r:
1981 base = r[EMRBlockPublicAccessConfiguration.annotation_key]
1982 else:
1983 try:
1984 base = client.get_block_public_access_configuration()
1985 base.pop('ResponseMetadata')
1986 except client.exceptions.NoSuchPublicAccessBlockConfiguration:
1987 base = {}
1989 config = base['BlockPublicAccessConfiguration']
1990 updatedConfig = {**config, **self.data.get('config')}
1992 if config == updatedConfig:
1993 return
1995 client.put_block_public_access_configuration(
1996 BlockPublicAccessConfiguration=updatedConfig
1997 )
2000@filters.register('securityhub')
2001class SecHubEnabled(Filter):
2002 """Filter an account depending on whether security hub is enabled or not.
2004 :example:
2006 .. code-block:: yaml
2008 policies:
2009 - name: check-securityhub-status
2010 resource: aws.account
2011 filters:
2012 - type: securityhub
2013 enabled: true
2015 """
2017 permissions = ('securityhub:DescribeHub',)
2019 schema = type_schema('securityhub', enabled={'type': 'boolean'})
2021 def process(self, resources, event=None):
2022 state = self.data.get('enabled', True)
2023 client = local_session(self.manager.session_factory).client('securityhub')
2024 sechub = self.manager.retry(client.describe_hub, ignore_err_codes=(
2025 'InvalidAccessException',))
2026 if state == bool(sechub):
2027 return resources
2028 return []
2031@filters.register('lakeformation-s3-cross-account')
2032class LakeformationFilter(Filter):
2033 """Flags an account if its using a lakeformation s3 bucket resource from a different account.
2035 :example:
2037 .. code-block:: yaml
2039 policies:
2040 - name: lakeformation-cross-account-bucket
2041 resource: aws.account
2042 filters:
2043 - type: lakeformation-s3-cross-account
2045 """
2047 schema = type_schema('lakeformation-s3-cross-account', rinherit=ValueFilter.schema)
2048 schema_alias = False
2049 permissions = ('lakeformation:ListResources',)
2050 annotation = 'c7n:lake-cross-account-s3'
2052 def process(self, resources, event=None):
2053 results = []
2054 for r in resources:
2055 if self.process_account(r):
2056 results.append(r)
2057 return results
2059 def process_account(self, account):
2060 client = local_session(self.manager.session_factory).client('lakeformation')
2061 lake_buckets = {
2062 Arn.parse(r).resource for r in jmespath_search(
2063 'ResourceInfoList[].ResourceArn',
2064 client.list_resources())
2065 }
2066 buckets = {
2067 b['Name'] for b in
2068 self.manager.get_resource_manager('s3').resources(augment=False)}
2069 cross_account = lake_buckets.difference(buckets)
2070 if not cross_account:
2071 return False
2072 account[self.annotation] = list(cross_account)
2073 return True
2076@actions.register('toggle-config-managed-rule')
2077class ToggleConfigManagedRule(BaseAction):
2078 """Enables or disables an AWS Config Managed Rule
2080 :example:
2082 .. code-block:: yaml
2084 policies:
2085 - name: config-managed-s3-bucket-public-write-remediate-event
2086 description: |
2087 This policy detects if S3 bucket allows public write by the bucket policy
2088 or ACL and remediates.
2089 comment: |
2090 This policy detects if S3 bucket policy or ACL allows public write access.
2091 When the bucket is evaluated as 'NON_COMPLIANT', the action
2092 'AWS-DisableS3BucketPublicReadWrite' is triggered and remediates.
2093 resource: account
2094 filters:
2095 - type: missing
2096 policy:
2097 resource: config-rule
2098 filters:
2099 - type: remediation
2100 rule_name: &rule_name 'config-managed-s3-bucket-public-write-remediate-event'
2101 remediation: &remediation-config
2102 TargetId: AWS-DisableS3BucketPublicReadWrite
2103 Automatic: true
2104 MaximumAutomaticAttempts: 5
2105 RetryAttemptSeconds: 211
2106 Parameters:
2107 AutomationAssumeRole:
2108 StaticValue:
2109 Values:
2110 - 'arn:aws:iam::{account_id}:role/myrole'
2111 S3BucketName:
2112 ResourceValue:
2113 Value: RESOURCE_ID
2114 actions:
2115 - type: toggle-config-managed-rule
2116 rule_name: *rule_name
2117 managed_rule_id: S3_BUCKET_PUBLIC_WRITE_PROHIBITED
2118 resource_types:
2119 - 'AWS::S3::Bucket'
2120 rule_parameters: '{}'
2121 remediation: *remediation-config
2122 """
2124 permissions = (
2125 'config:DescribeConfigRules',
2126 'config:DescribeRemediationConfigurations',
2127 'config:PutRemediationConfigurations',
2128 'config:PutConfigRule',
2129 )
2131 schema = type_schema('toggle-config-managed-rule',
2132 enabled={'type': 'boolean', 'default': True},
2133 rule_name={'type': 'string'},
2134 rule_prefix={'type': 'string'},
2135 managed_rule_id={'type': 'string'},
2136 resource_types={'type': 'array', 'items':
2137 {'pattern': '^AWS::*', 'type': 'string'}},
2138 resource_tag={
2139 'type': 'object',
2140 'properties': {
2141 'key': {'type': 'string'},
2142 'value': {'type': 'string'},
2143 },
2144 'required': ['key', 'value'],
2145 },
2146 resource_id={'type': 'string'},
2147 rule_parameters={'type': 'string'},
2148 remediation={
2149 'type': 'object',
2150 'properties': {
2151 'TargetType': {'type': 'string'},
2152 'TargetId': {'type': 'string'},
2153 'Automatic': {'type': 'boolean'},
2154 'Parameters': {'type': 'object'},
2155 'MaximumAutomaticAttempts': {
2156 'type': 'integer',
2157 'minimum': 1, 'maximum': 25,
2158 },
2159 'RetryAttemptSeconds': {
2160 'type': 'integer',
2161 'minimum': 1, 'maximum': 2678000,
2162 },
2163 'ExecutionControls': {'type': 'object'},
2164 },
2165 },
2166 tags={'type': 'object'},
2167 required=['rule_name'],
2168 )
2170 def validate(self):
2171 if (
2172 self.data.get('enabled', True) and
2173 not self.data.get('managed_rule_id')
2174 ):
2175 raise PolicyValidationError("managed_rule_id required to enable a managed rule")
2176 return self
2178 def process(self, accounts):
2179 client = local_session(self.manager.session_factory).client('config')
2180 rule = self.ConfigManagedRule(self.data)
2181 params = self.get_rule_params(rule)
2183 if self.data.get('enabled', True):
2184 client.put_config_rule(**params)
2186 if rule.remediation:
2187 remediation_params = self.get_remediation_params(rule)
2188 client.put_remediation_configurations(
2189 RemediationConfigurations=[remediation_params]
2190 )
2191 else:
2192 with suppress(client.exceptions.NoSuchRemediationConfigurationException):
2193 client.delete_remediation_configuration(
2194 ConfigRuleName=rule.name
2195 )
2197 with suppress(client.exceptions.NoSuchConfigRuleException):
2198 client.delete_config_rule(
2199 ConfigRuleName=rule.name
2200 )
2202 def get_rule_params(self, rule):
2203 params = dict(
2204 ConfigRuleName=rule.name,
2205 Description=rule.description,
2206 Source={
2207 'Owner': 'AWS',
2208 'SourceIdentifier': rule.managed_rule_id,
2209 },
2210 InputParameters=rule.rule_parameters
2211 )
2213 # A config rule scope can include one or more resource types,
2214 # a combination of a tag key and value, or a combination of
2215 # one resource type and one resource ID
2216 params.update({'Scope': {'ComplianceResourceTypes': rule.resource_types}})
2217 if rule.resource_tag:
2218 params.update({'Scope': {
2219 'TagKey': rule.resource_tag['key'],
2220 'TagValue': rule.resource_tag['value']}
2221 })
2222 elif rule.resource_id:
2223 params.update({'Scope': {'ComplianceResourceId': rule.resource_id}})
2225 return dict(ConfigRule=params)
2227 def get_remediation_params(self, rule):
2228 rule.remediation['ConfigRuleName'] = rule.name
2229 if 'TargetType' not in rule.remediation:
2230 rule.remediation['TargetType'] = 'SSM_DOCUMENT'
2231 return rule.remediation
2233 class ConfigManagedRule:
2234 """Wraps the action data into an AWS Config Managed Rule.
2235 """
2237 def __init__(self, data):
2238 self.data = data
2240 @property
2241 def name(self):
2242 prefix = self.data.get('rule_prefix', 'custodian-')
2243 return "%s%s" % (prefix, self.data.get('rule_name', ''))
2245 @property
2246 def description(self):
2247 return self.data.get(
2248 'description', 'cloud-custodian AWS Config Managed Rule policy')
2250 @property
2251 def tags(self):
2252 return self.data.get('tags', {})
2254 @property
2255 def resource_types(self):
2256 return self.data.get('resource_types', [])
2258 @property
2259 def managed_rule_id(self):
2260 return self.data.get('managed_rule_id', '')
2262 @property
2263 def resource_tag(self):
2264 return self.data.get('resource_tag', {})
2266 @property
2267 def resource_id(self):
2268 return self.data.get('resource_id', '')
2270 @property
2271 def rule_parameters(self):
2272 return self.data.get('rule_parameters', '')
2274 @property
2275 def remediation(self):
2276 return self.data.get('remediation', {})
2279@filters.register('ses-agg-send-stats')
2280class SesAggStats(ValueFilter):
2281 """This filter queries SES send statistics and aggregates all
2282 the data points into a single report.
2284 :example:
2286 .. code-block:: yaml
2288 policies:
2289 - name: ses-aggregated-send-stats-policy
2290 resource: account
2291 filters:
2292 - type: ses-agg-send-stats
2293 """
2295 schema = type_schema('ses-agg-send-stats', rinherit=ValueFilter.schema)
2296 annotation_key = 'c7n:ses-send-agg'
2297 permissions = ("ses:GetSendStatistics",)
2299 def process(self, resources, event=None):
2300 client = local_session(self.manager.session_factory).client('ses')
2301 get_send_stats = client.get_send_statistics()
2302 results = []
2304 if not get_send_stats or not get_send_stats.get('SendDataPoints'):
2305 return results
2307 resource_counter = {'DeliveryAttempts': 0,
2308 'Bounces': 0,
2309 'Complaints': 0,
2310 'Rejects': 0,
2311 'BounceRate': 0}
2312 for d in get_send_stats.get('SendDataPoints', []):
2313 resource_counter['DeliveryAttempts'] += d['DeliveryAttempts']
2314 resource_counter['Bounces'] += d['Bounces']
2315 resource_counter['Complaints'] += d['Complaints']
2316 resource_counter['Rejects'] += d['Rejects']
2317 resource_counter['BounceRate'] = round(
2318 (resource_counter['Bounces'] /
2319 resource_counter['DeliveryAttempts']) * 100)
2320 resources[0][self.annotation_key] = resource_counter
2322 return resources
2325@filters.register('ses-send-stats')
2326class SesConsecutiveStats(Filter):
2327 """This filter annotates the account resource with SES send statistics for the
2328 last n number of days, not including the current date.
2330 The stats are aggregated into daily metrics. Additionally, the filter also
2331 calculates and annotates the max daily bounce rate (percentage). Using this filter,
2332 users can alert when the bounce rate for a particular day is higher than the limit.
2334 :example:
2336 .. code-block:: yaml
2338 policies:
2339 - name: ses-send-stats
2340 resource: account
2341 filters:
2342 - type: ses-send-stats
2343 days: 5
2344 - type: value
2345 key: '"c7n:ses-max-bounce-rate"'
2346 op: ge
2347 value: 10
2348 """
2349 schema = type_schema('ses-send-stats', days={'type': 'number', 'minimum': 2},
2350 required=['days'])
2351 send_stats_annotation = 'c7n:ses-send-stats'
2352 max_bounce_annotation = 'c7n:ses-max-bounce-rate'
2353 permissions = ("ses:GetSendStatistics",)
2355 def process(self, resources, event=None):
2356 client = local_session(self.manager.session_factory).client('ses')
2357 get_send_stats = client.get_send_statistics()
2358 results = []
2359 check_days = self.data.get('days', 2)
2360 utcnow = datetime.datetime.utcnow()
2361 expected_dates = set()
2363 for days in range(1, check_days + 1):
2364 expected_dates.add((utcnow - datetime.timedelta(days=days)).strftime('%Y-%m-%d'))
2366 if not get_send_stats or not get_send_stats.get('SendDataPoints'):
2367 return results
2369 metrics = {}
2370 for d in get_send_stats.get('SendDataPoints', []):
2371 ts = d['Timestamp'].strftime('%Y-%m-%d')
2372 if ts not in expected_dates:
2373 continue
2375 if not metrics.get(ts):
2376 metrics[ts] = {'DeliveryAttempts': 0,
2377 'Bounces': 0,
2378 'Complaints': 0,
2379 'Rejects': 0}
2380 metrics[ts]['DeliveryAttempts'] += d['DeliveryAttempts']
2381 metrics[ts]['Bounces'] += d['Bounces']
2382 metrics[ts]['Complaints'] += d['Complaints']
2383 metrics[ts]['Rejects'] += d['Rejects']
2385 max_bounce_rate = 0
2386 for ts, metric in metrics.items():
2387 metric['BounceRate'] = round((metric['Bounces'] / metric['DeliveryAttempts']) * 100)
2388 if max_bounce_rate < metric['BounceRate']:
2389 max_bounce_rate = metric['BounceRate']
2390 metric['Date'] = ts
2392 resources[0][self.send_stats_annotation] = list(metrics.values())
2393 resources[0][self.max_bounce_annotation] = max_bounce_rate
2395 return resources
2398@filters.register('bedrock-model-invocation-logging')
2399class BedrockModelInvocationLogging(ListItemFilter):
2400 """Filter for account to look at bedrock model invocation logging configuration
2402 The schema to supply to the attrs follows the schema here:
2403 https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock/client/get_model_invocation_logging_configuration.html
2405 :example:
2407 .. code-block:: yaml
2409 policies:
2410 - name: bedrock-model-invocation-logging-configuration
2411 resource: account
2412 filters:
2413 - type: bedrock-model-invocation-logging
2414 attrs:
2415 - imageDataDeliveryEnabled: True
2417 """
2418 schema = type_schema(
2419 'bedrock-model-invocation-logging',
2420 attrs={'$ref': '#/definitions/filters_common/list_item_attrs'},
2421 count={'type': 'number'},
2422 count_op={'$ref': '#/definitions/filters_common/comparison_operators'}
2423 )
2424 permissions = ('bedrock:GetModelInvocationLoggingConfiguration',)
2425 annotation_key = 'c7n:BedrockModelInvocationLogging'
2427 def get_item_values(self, resource):
2428 item_values = []
2429 client = local_session(self.manager.session_factory).client('bedrock')
2430 invocation_logging_config = client \
2431 .get_model_invocation_logging_configuration().get('loggingConfig')
2432 if invocation_logging_config is not None:
2433 item_values.append(invocation_logging_config)
2434 resource[self.annotation_key] = invocation_logging_config
2435 return item_values
2438@actions.register('set-bedrock-model-invocation-logging')
2439class SetBedrockModelInvocationLogging(BaseAction):
2440 """Set Bedrock Model Invocation Logging Configuration on an account.
2441 https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock/client/put_model_invocation_logging_configuration.html
2443 To delete a configuration, supply enabled to False
2445 :example:
2447 .. code-block:: yaml
2449 policies:
2450 - name: set-bedrock-model-invocation-logging
2451 resource: account
2452 actions:
2453 - type: set-bedrock-model-invocation-logging
2454 enabled: True
2455 loggingConfig:
2456 textDataDeliveryEnabled: True
2457 s3Config:
2458 bucketName: test-bedrock-1
2459 keyPrefix: logging/
2461 - name: delete-bedrock-model-invocation-logging
2462 resource: account
2463 actions:
2464 - type: set-bedrock-model-invocation-logging
2465 enabled: False
2466 """
2468 schema = {
2469 'type': 'object',
2470 'additionalProperties': False,
2471 'properties': {
2472 'type': {'enum': ['set-bedrock-model-invocation-logging']},
2473 'enabled': {'type': 'boolean'},
2474 'loggingConfig': {'type': 'object'}
2475 },
2476 }
2478 permissions = ('bedrock:PutModelInvocationLoggingConfiguration',)
2479 shape = 'PutModelInvocationLoggingConfigurationRequest'
2480 service = 'bedrock'
2482 def validate(self):
2483 cfg = dict(self.data)
2484 enabled = cfg.get('enabled')
2485 if enabled:
2486 cfg.pop('type')
2487 cfg.pop('enabled')
2488 return shape_validate(
2489 cfg,
2490 self.shape,
2491 self.service)
2493 def process(self, resources):
2494 client = local_session(self.manager.session_factory).client('bedrock')
2495 if self.data.get('enabled'):
2496 params = self.data.get('loggingConfig')
2497 client.put_model_invocation_logging_configuration(loggingConfig=params)
2498 else:
2499 client.delete_model_invocation_logging_configuration()