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
13
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
24
25from c7n.resources.iam import CredentialReport
26from c7n.resources.securityhub import OtherResourcePostFinding
27
28from .aws import shape_validate, Arn
29
30filters = FilterRegistry('aws.account.filters')
31actions = ActionRegistry('aws.account.actions')
32
33retry = staticmethod(QueryResourceManager.retry)
34filters.register('missing', Missing)
35
36
37class DescribeAccount(DescribeSource):
38
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}
46
47 def resources(self, query=None):
48 return [self.get_account()]
49
50 def get_resources(self, resource_ids):
51 return [self.get_account()]
52
53
54@resources.register('account')
55class Account(QueryResourceManager):
56
57 filter_registry = filters
58 action_registry = actions
59 retry = staticmethod(QueryResourceManager.retry)
60 source_type = 'describe'
61
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'
71
72 source_mapping = {
73 'describe': DescribeAccount
74 }
75
76 @classmethod
77 def get_permissions(cls):
78 return ('iam:ListAccountAliases',)
79
80 @classmethod
81 def has_arn(cls):
82 return True
83
84 def get_arns(self, resources):
85 return ["arn:::{account_id}".format(**r) for r in resources]
86
87
88@filters.register('credential')
89class AccountCredentialReport(CredentialReport):
90
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
103
104
105@filters.register('organization')
106class AccountOrganization(ValueFilter):
107 """Check organization enrollment and configuration
108
109 :example:
110
111 determine if an account is not in an organization
112
113 .. code-block:: yaml
114
115 policies:
116 - name: no-org
117 resource: account
118 filters:
119 - type: organization
120 key: Id
121 value: absent
122
123
124 :example:
125
126 determine if an account is setup for organization policies
127
128 .. code-block:: yaml
129
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
141
142 annotation_key = 'c7n:org'
143 annotate = False
144
145 permissions = ('organizations:DescribeOrganization',)
146
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
158
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 []
168
169
170@filters.register('check-macie')
171class MacieEnabled(ValueFilter):
172 """Check status of macie v2 in the account.
173
174 Gets the macie session info for the account, and
175 the macie master account for the current account if
176 configured.
177 """
178
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',)
184
185 def process(self, resources, event=None):
186
187 if self.annotation_key not in resources[0]:
188 self.get_macie_info(resources[0])
189
190 if super().process([resources[0][self.annotation_key]]):
191 return resources
192
193 return []
194
195 def get_macie_info(self, account):
196 client = local_session(
197 self.manager.session_factory).client('macie2')
198
199 try:
200 info = client.get_macie_session()
201 info.pop('ResponseMetadata')
202 except client.exceptions.AccessDeniedException:
203 info = {}
204
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
213
214
215@filters.register('check-cloudtrail')
216class CloudTrailEnabled(Filter):
217 """Verify cloud trail enabled for this account per specifications.
218
219 Returns an annotated account resource if trail is not enabled.
220
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.
223
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
226
227 :example:
228
229 .. code-block:: yaml
230
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\\") }"
242
243 Check for CloudWatch log group with a metric filter that has a filter pattern
244 matching a regex pattern:
245
246 .. code-block:: yaml
247
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'}]}})
273
274 permissions = ('cloudtrail:DescribeTrails', 'cloudtrail:GetTrailStatus',
275 'cloudtrail:GetEventSelectors', 'cloudwatch:DescribeAlarmsForMetric',
276 'logs:DescribeMetricFilters', 'sns:GetTopicAttributes')
277
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
283
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)
331
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)
344
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
388
389
390@filters.register('guard-duty')
391class GuardDutyEnabled(MultiAttrFilter):
392 """Check if the guard duty service is enabled.
393
394 This allows looking at account's detector and its associated
395 master if any.
396
397 :example:
398
399 Check to ensure guard duty is active on account and associated to a master.
400
401 .. code-block:: yaml
402
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 """
412
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 }
423
424 annotation = "c7n:guard-duty"
425 permissions = (
426 'guardduty:GetMasterAccount',
427 'guardduty:ListDetectors',
428 'guardduty:GetDetector')
429
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()
437
438 def get_target(self, resource):
439 if self.annotation in resource:
440 return resource[self.annotation]
441
442 client = local_session(self.manager.session_factory).client('guardduty')
443 # detectors are singletons too.
444 detector_ids = client.list_detectors().get('DetectorIds')
445
446 if not detector_ids:
447 return None
448 else:
449 detector_id = detector_ids.pop()
450
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
456
457
458@filters.register('check-config')
459class ConfigEnabled(Filter):
460 """Is config service enabled for this account
461
462 :example:
463
464 .. code-block:: yaml
465
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 """
476
477 schema = type_schema(
478 'check-config', **{
479 'all-resources': {'type': 'boolean'},
480 'running': {'type': 'boolean'},
481 'global-resources': {'type': 'boolean'}})
482
483 permissions = ('config:DescribeDeliveryChannels',
484 'config:DescribeConfigurationRecorders',
485 'config:DescribeConfigurationRecorderStatus')
486
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
513
514
515@filters.register('iam-summary')
516class IAMSummary(ValueFilter):
517 """Return annotated account resource if iam summary filter matches.
518
519 Some use cases include, detecting root api keys or mfa usage.
520
521 Example iam summary wrt to matchable fields::
522
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 }
557
558 For example to determine if an account has either not been
559 enabled with root mfa or has root api keys.
560
561 .. code-block:: yaml
562
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',)
576
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 []
586
587
588@filters.register('access-analyzer')
589class AccessAnalyzer(ValueFilter):
590 """Check for access analyzers in an account
591
592 :example:
593
594 .. code-block:: yaml
595
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 """
605
606 schema = type_schema('access-analyzer', rinherit=ValueFilter.schema)
607 schema_alias = False
608 permissions = ('access-analyzer:ListAnalyzers',)
609 annotation_key = 'c7n:matched-analyzers'
610
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)
618
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 []
625
626
627@filters.register('password-policy')
628class AccountPasswordPolicy(ValueFilter):
629 """Check an account's password policy.
630
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.
634
635 :example:
636
637 .. code-block:: yaml
638
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',)
655
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 []
673
674
675@actions.register('set-password-policy')
676class SetAccountPasswordPolicy(BaseAction):
677 """Set an account's password policy.
678
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
682
683 :example:
684
685 .. code-block:: yaml
686
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')
709
710 def validate(self):
711 return shape_validate(
712 self.data.get('policy', {}),
713 self.shape,
714 self.service)
715
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)
731
732
733@filters.register('service-limit')
734class ServiceLimit(Filter):
735 """Check if account's service limits are past a given threshold.
736
737 Supported limits are per trusted advisor, which is variable based
738 on usage in the account and support level enabled on the account.
739
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*`.
743
744 The names are exactly what's shown on the trusted advisor page:
745
746 https://console.aws.amazon.com/trustedadvisor/home#/category/service-limits
747
748 or via the awscli:
749
750 aws --region us-east-1 support describe-trusted-advisor-checks --language en \
751 --query 'checks[?category==`service_limits`].[name]' --output text
752
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:
756
757 - `services`
758
759 The resulting limit's `service` field must match one of these.
760 These are case-insensitive globbing matches.
761
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.
766
767 - `limits`
768
769 The resulting limit's `Limit Name` field must match one of these.
770 These are case-insensitive globbing matches.
771
772 Some example names and their corresponding service and limit names:
773
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
785
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.
789
790 :example:
791
792 .. code-block:: yaml
793
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
804
805 - name: increase-account-service-limits
806 resource: account
807 filters:
808 - type: service-limit
809 services:
810 - EC2
811 threshold: 1.0
812
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 """
823
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']}})
835
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')
842
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'}
848
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
857
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']
862
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
872
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
882
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]
888
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
894
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)
900
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(' ', ''))
906
907 return True
908
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 []
928
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'])
932
933 if results is None or 'flaggedResources' not in results:
934 return []
935
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 ]
942
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)
947
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
959
960 services = self.data.get('services')
961 limits = self.data.get('limits')
962 threshold = self.data.get('threshold')
963 exceeded = []
964
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
981
982
983@actions.register('request-limit-increase')
984class RequestLimitIncrease(BaseAction):
985 r"""File support ticket to raise limit.
986
987 :Example:
988
989 .. code-block:: yaml
990
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 """
1008
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 }
1027
1028 permissions = ('support:CreateCase',)
1029
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'
1033
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 }
1048
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)
1060
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'])
1075
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', []))
1092
1093
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)
1131
1132
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)
1137
1138
1139@actions.register('enable-cloudtrail')
1140class EnableTrail(BaseAction):
1141 """Enables logging on the trail(s) named in the policy
1142
1143 :Example:
1144
1145 .. code-block:: yaml
1146
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 """
1156
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 )
1182
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', '')
1196
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
1207
1208 try:
1209 current_policy = s3client.get_bucket_policy(Bucket=bucket_name)
1210 except ClientError:
1211 current_policy = None
1212
1213 policy_json = cloudtrail_policy(
1214 current_policy, bucket_name,
1215 self.manager.config.account_id, bucket_region)
1216
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)
1252
1253
1254@filters.register('has-virtual-mfa')
1255class HasVirtualMFA(Filter):
1256 """Is the account configured with a virtual MFA device?
1257
1258 :example:
1259
1260 .. code-block:: yaml
1261
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 """
1270
1271 schema = type_schema('has-virtual-mfa', **{'value': {'type': 'boolean'}})
1272
1273 permissions = ('iam:ListVirtualMFADevices',)
1274
1275 def mfa_belongs_to_root_account(self, mfa):
1276 return mfa['SerialNumber'].endswith(':mfa/root-account-mfa-device')
1277
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
1288
1289 def process(self, resources, event=None):
1290 return list(filter(self.account_has_virtual_mfa, resources))
1291
1292
1293@actions.register('enable-data-events')
1294class EnableDataEvents(BaseAction):
1295 """Ensure all buckets in account are setup to log data events.
1296
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/
1299
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.
1303
1304 :example:
1305
1306 .. code-block:: yaml
1307
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 """
1317
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'}}}})
1346
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
1354
1355 def get_permissions(self):
1356 perms = [
1357 'cloudtrail:DescribeTrails',
1358 'cloudtrail:GetEventSelectors',
1359 'cloudtrail:PutEventSelectors']
1360
1361 if self.data.get('data-trail', {}).get('create'):
1362 perms.extend([
1363 'cloudtrail:CreateTrail', 'cloudtrail:StartLogging'])
1364 return perms
1365
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)
1374
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
1383
1384 client.create_trail(**params)
1385 return {'Name': trail_cfg['name']}
1386
1387 def process(self, resources):
1388 session = local_session(self.manager.session_factory)
1389 region = self.data['data-trail'].get('multi-region')
1390
1391 if region:
1392 client = session.client('cloudtrail', region_name=region)
1393 else:
1394 client = session.client('cloudtrail')
1395
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]
1405
1406 events = client.get_event_selectors(
1407 TrailName=trail['Name']).get('EventSelectors', [])
1408
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
1423
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")
1429
1430 # future proof'd for other data events, for s3 this trail
1431 # encompasses all the buckets in the account.
1432
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)
1442
1443 if added:
1444 client.start_logging(Name=tconfig['name'])
1445
1446 resources[0]['c7n_data_trail'] = trail
1447
1448
1449@filters.register('shield-enabled')
1450class ShieldEnabled(Filter):
1451
1452 permissions = ('shield:DescribeSubscription',)
1453
1454 schema = type_schema(
1455 'shield-enabled',
1456 state={'type': 'boolean'})
1457
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
1468
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 []
1475
1476
1477@actions.register('set-shield-advanced')
1478class SetShieldAdvanced(BaseAction):
1479 """Enable/disable Shield Advanced on an account."""
1480
1481 permissions = (
1482 'shield:CreateSubscription', 'shield:DeleteSubscription')
1483
1484 schema = type_schema(
1485 'set-shield-advanced',
1486 state={'type': 'boolean'})
1487
1488 def process(self, resources):
1489 client = local_session(self.manager.session_factory).client('shield')
1490 state = self.data.get('state', True)
1491
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
1501
1502
1503@filters.register('xray-encrypt-key')
1504class XrayEncrypted(Filter):
1505 """Determine if xray is encrypted.
1506
1507 :example:
1508
1509 .. code-block:: yaml
1510
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 """
1528
1529 permissions = ('xray:GetEncryptionConfig',)
1530 schema = type_schema(
1531 'xray-encrypt-key',
1532 required=['key'],
1533 key={'type': 'string'}
1534 )
1535
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
1540
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
1550
1551
1552@actions.register('set-xray-encrypt')
1553class SetXrayEncryption(BaseAction):
1554 """Enable specific xray encryption.
1555
1556 :example:
1557
1558 .. code-block:: yaml
1559
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 """
1572
1573 permissions = ('xray:PutEncryptionConfig',)
1574 schema = type_schema(
1575 'set-xray-encrypt',
1576 required=['key'],
1577 key={'type': 'string'}
1578 )
1579
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)
1585
1586
1587@filters.register('default-ebs-encryption')
1588class EbsEncryption(Filter):
1589 """Filter an account by its ebs encryption status.
1590
1591 By default for key we match on the alias name for a key.
1592
1593 :example:
1594
1595 .. code-block:: yaml
1596
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
1604
1605 It is also possible to match on specific key attributes (tags, origin)
1606
1607 :example:
1608
1609 .. code-block:: yaml
1610
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'}]})
1629
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
1647
1648
1649@actions.register('set-ebs-encryption')
1650class SetEbsEncryption(BaseAction):
1651 """Set AWS EBS default encryption on an account
1652
1653 :example:
1654
1655 .. code-block:: yaml
1656
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')
1670
1671 schema = type_schema(
1672 'set-ebs-encryption',
1673 state={'type': 'boolean'},
1674 key={'type': 'string'})
1675
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()
1685
1686 if state and key:
1687 client.modify_ebs_default_kms_key_id(
1688 KmsKeyId=self.data['key'])
1689
1690
1691@filters.register('s3-public-block')
1692class S3PublicBlock(ValueFilter):
1693 """Check for s3 public blocks on an account.
1694
1695 https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html
1696 """
1697
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',)
1703
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)
1707
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] = {}
1716
1717 def __call__(self, r):
1718 return super(S3PublicBlock, self).__call__(r[self.annotation_key])
1719
1720
1721@actions.register('set-s3-public-block')
1722class SetS3PublicBlock(BaseAction):
1723 """Configure S3 Public Access Block on an account.
1724
1725 All public access block attributes can be set. If not specified they are merged
1726 with the extant configuration.
1727
1728 https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html
1729
1730 :example:
1731
1732 .. yaml:
1733
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
1745
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'})
1754
1755 permissions = ('s3:PutAccountPublicAccessBlock', 's3:GetAccountPublicAccessBlock')
1756
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))
1764
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
1771
1772 keys = (
1773 'BlockPublicPolicy', 'BlockPublicAcls', 'IgnorePublicAcls', 'RestrictPublicBuckets')
1774
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 = {}
1786
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]
1793
1794 client.put_public_access_block(
1795 AccountId=r['account_id'],
1796 PublicAccessBlockConfiguration=config)
1797
1798
1799class GlueCatalogEncryptionEnabled(MultiAttrFilter):
1800 """ Filter glue catalog by its glue encryption status and KMS key
1801
1802 :example:
1803
1804 .. code-block:: yaml
1805
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
1812
1813 """
1814 retry = staticmethod(QueryResourceManager.retry)
1815
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 }
1827
1828 annotation = "c7n:glue-security-config"
1829 permissions = ('glue:GetDataCatalogEncryptionSettings',)
1830
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()
1841
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]
1865
1866
1867@filters.register('glue-security-config')
1868class AccountCatalogEncryptionFilter(GlueCatalogEncryptionEnabled):
1869 """Filter aws account by its glue encryption status and KMS key
1870
1871 :example:
1872
1873 .. code-block:: yaml
1874
1875 policies:
1876 - name: glue-security-config
1877 resource: aws.account
1878 filters:
1879 - type: glue-security-config
1880 SseAwsKmsKeyId: alias/aws/glue
1881
1882 """
1883
1884
1885@filters.register('emr-block-public-access')
1886class EMRBlockPublicAccessConfiguration(ValueFilter):
1887 """Check for EMR block public access configuration on an account
1888
1889 :example:
1890
1891 .. code-block:: yaml
1892
1893 policies:
1894 - name: get-emr-block-public-access
1895 resource: account
1896 filters:
1897 - type: emr-block-public-access
1898 """
1899
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",)
1905
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)
1909
1910 def augment(self, resources):
1911 client = local_session(self.manager.session_factory).client(
1912 'emr', region_name=self.manager.config.region)
1913
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] = {}
1920
1921 def __call__(self, r):
1922 return super(EMRBlockPublicAccessConfiguration, self).__call__(r[self.annotation_key])
1923
1924
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
1929
1930 :example:
1931
1932 .. code-block:: yaml
1933
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
1950
1951 """
1952
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',))
1972
1973 permissions = ("elasticmapreduce:PutBlockPublicAccessConfiguration",)
1974
1975 def process(self, resources):
1976 client = local_session(self.manager.session_factory).client('emr')
1977 r = resources[0]
1978
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 = {}
1988
1989 config = base['BlockPublicAccessConfiguration']
1990 updatedConfig = {**config, **self.data.get('config')}
1991
1992 if config == updatedConfig:
1993 return
1994
1995 client.put_block_public_access_configuration(
1996 BlockPublicAccessConfiguration=updatedConfig
1997 )
1998
1999
2000@filters.register('securityhub')
2001class SecHubEnabled(Filter):
2002 """Filter an account depending on whether security hub is enabled or not.
2003
2004 :example:
2005
2006 .. code-block:: yaml
2007
2008 policies:
2009 - name: check-securityhub-status
2010 resource: aws.account
2011 filters:
2012 - type: securityhub
2013 enabled: true
2014
2015 """
2016
2017 permissions = ('securityhub:DescribeHub',)
2018
2019 schema = type_schema('securityhub', enabled={'type': 'boolean'})
2020
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 []
2029
2030
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.
2034
2035 :example:
2036
2037 .. code-block:: yaml
2038
2039 policies:
2040 - name: lakeformation-cross-account-bucket
2041 resource: aws.account
2042 filters:
2043 - type: lakeformation-s3-cross-account
2044
2045 """
2046
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'
2051
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
2058
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
2074
2075
2076@actions.register('toggle-config-managed-rule')
2077class ToggleConfigManagedRule(BaseAction):
2078 """Enables or disables an AWS Config Managed Rule
2079
2080 :example:
2081
2082 .. code-block:: yaml
2083
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 """
2123
2124 permissions = (
2125 'config:DescribeConfigRules',
2126 'config:DescribeRemediationConfigurations',
2127 'config:PutRemediationConfigurations',
2128 'config:PutConfigRule',
2129 )
2130
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 )
2169
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
2177
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)
2182
2183 if self.data.get('enabled', True):
2184 client.put_config_rule(**params)
2185
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 )
2196
2197 with suppress(client.exceptions.NoSuchConfigRuleException):
2198 client.delete_config_rule(
2199 ConfigRuleName=rule.name
2200 )
2201
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 )
2212
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}})
2224
2225 return dict(ConfigRule=params)
2226
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
2232
2233 class ConfigManagedRule:
2234 """Wraps the action data into an AWS Config Managed Rule.
2235 """
2236
2237 def __init__(self, data):
2238 self.data = data
2239
2240 @property
2241 def name(self):
2242 prefix = self.data.get('rule_prefix', 'custodian-')
2243 return "%s%s" % (prefix, self.data.get('rule_name', ''))
2244
2245 @property
2246 def description(self):
2247 return self.data.get(
2248 'description', 'cloud-custodian AWS Config Managed Rule policy')
2249
2250 @property
2251 def tags(self):
2252 return self.data.get('tags', {})
2253
2254 @property
2255 def resource_types(self):
2256 return self.data.get('resource_types', [])
2257
2258 @property
2259 def managed_rule_id(self):
2260 return self.data.get('managed_rule_id', '')
2261
2262 @property
2263 def resource_tag(self):
2264 return self.data.get('resource_tag', {})
2265
2266 @property
2267 def resource_id(self):
2268 return self.data.get('resource_id', '')
2269
2270 @property
2271 def rule_parameters(self):
2272 return self.data.get('rule_parameters', '')
2273
2274 @property
2275 def remediation(self):
2276 return self.data.get('remediation', {})
2277
2278
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.
2283
2284 :example:
2285
2286 .. code-block:: yaml
2287
2288 policies:
2289 - name: ses-aggregated-send-stats-policy
2290 resource: account
2291 filters:
2292 - type: ses-agg-send-stats
2293 """
2294
2295 schema = type_schema('ses-agg-send-stats', rinherit=ValueFilter.schema)
2296 annotation_key = 'c7n:ses-send-agg'
2297 permissions = ("ses:GetSendStatistics",)
2298
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 = []
2303
2304 if not get_send_stats or not get_send_stats.get('SendDataPoints'):
2305 return results
2306
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
2321
2322 return resources
2323
2324
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.
2329
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.
2333
2334 :example:
2335
2336 .. code-block:: yaml
2337
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",)
2354
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()
2362
2363 for days in range(1, check_days + 1):
2364 expected_dates.add((utcnow - datetime.timedelta(days=days)).strftime('%Y-%m-%d'))
2365
2366 if not get_send_stats or not get_send_stats.get('SendDataPoints'):
2367 return results
2368
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
2374
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']
2384
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
2391
2392 resources[0][self.send_stats_annotation] = list(metrics.values())
2393 resources[0][self.max_bounce_annotation] = max_bounce_rate
2394
2395 return resources
2396
2397
2398@filters.register('bedrock-model-invocation-logging')
2399class BedrockModelInvocationLogging(ListItemFilter):
2400 """Filter for account to look at bedrock model invocation logging configuration
2401
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
2404
2405 :example:
2406
2407 .. code-block:: yaml
2408
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
2416
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'
2426
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
2436
2437
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
2442
2443 To delete a configuration, supply enabled to False
2444
2445 :example:
2446
2447 .. code-block:: yaml
2448
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/
2460
2461 - name: delete-bedrock-model-invocation-logging
2462 resource: account
2463 actions:
2464 - type: set-bedrock-model-invocation-logging
2465 enabled: False
2466 """
2467
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 }
2477
2478 permissions = ('bedrock:PutModelInvocationLoggingConfiguration',)
2479 shape = 'PutModelInvocationLoggingConfigurationRequest'
2480 service = 'bedrock'
2481
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)
2492
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()