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 adminstrator 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:GetAdministratorAccount',)
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_administrator_account().get('administrator')
207 except (client.exceptions.AccessDeniedException,
208 client.exceptions.ResourceNotFoundException):
209 info['master'] = {}
210 else:
211 info['master'] = minfo
212 info['administrator'] = info['master']
213 account[self.annotation_key] = info
214
215
216@filters.register('check-cloudtrail')
217class CloudTrailEnabled(Filter):
218 """Verify cloud trail enabled for this account per specifications.
219
220 Returns an annotated account resource if trail is not enabled.
221
222 Of particular note, the current-region option will evaluate whether cloudtrail is available
223 in the current region, either as a multi region trail or as a trail with it as the home region.
224
225 The log-metric-filter-pattern option checks for the existence of a cloudwatch alarm and a
226 corresponding SNS subscription for a specific filter pattern
227
228 :example:
229
230 .. code-block:: yaml
231
232 policies:
233 - name: account-cloudtrail-enabled
234 resource: account
235 region: us-east-1
236 filters:
237 - type: check-cloudtrail
238 global-events: true
239 multi-region: true
240 running: true
241 include-management-events: true
242 log-metric-filter-pattern: "{ ($.eventName = \\"ConsoleLogin\\") }"
243
244 Check for CloudWatch log group with a metric filter that has a filter pattern
245 matching a regex pattern:
246
247 .. code-block:: yaml
248
249 policies:
250 - name: account-cloudtrail-with-matching-log-metric-filter
251 resource: account
252 region: us-east-1
253 filters:
254 - type: check-cloudtrail
255 log-metric-filter-pattern:
256 type: value
257 op: regex
258 value: '\\{ ?(\\()? ?\\$\\.eventName ?= ?(")?ConsoleLogin(")? ?(\\))? ?\\}'
259 """
260 schema = type_schema(
261 'check-cloudtrail',
262 **{'multi-region': {'type': 'boolean'},
263 'global-events': {'type': 'boolean'},
264 'current-region': {'type': 'boolean'},
265 'running': {'type': 'boolean'},
266 'notifies': {'type': 'boolean'},
267 'file-digest': {'type': 'boolean'},
268 'kms': {'type': 'boolean'},
269 'kms-key': {'type': 'string'},
270 'include-management-events': {'type': 'boolean'},
271 'log-metric-filter-pattern': {'oneOf': [
272 {'$ref': '#/definitions/filters/value'},
273 {'type': 'string'}]}})
274
275 permissions = ('cloudtrail:DescribeTrails', 'cloudtrail:GetTrailStatus',
276 'cloudtrail:GetEventSelectors', 'cloudwatch:DescribeAlarmsForMetric',
277 'logs:DescribeMetricFilters', 'sns:GetTopicAttributes')
278
279 def process(self, resources, event=None):
280 session = local_session(self.manager.session_factory)
281 client = session.client('cloudtrail')
282 trails = client.describe_trails()['trailList']
283 resources[0]['c7n:cloudtrails'] = trails
284
285 if self.data.get('global-events'):
286 trails = [t for t in trails if t.get('IncludeGlobalServiceEvents')]
287 if self.data.get('current-region'):
288 current_region = session.region_name
289 trails = [t for t in trails if t.get(
290 'HomeRegion') == current_region or t.get('IsMultiRegionTrail')]
291 if self.data.get('kms'):
292 trails = [t for t in trails if t.get('KmsKeyId')]
293 if self.data.get('kms-key'):
294 trails = [t for t in trails
295 if t.get('KmsKeyId', '') == self.data['kms-key']]
296 if self.data.get('file-digest'):
297 trails = [t for t in trails
298 if t.get('LogFileValidationEnabled')]
299 if self.data.get('multi-region'):
300 trails = [t for t in trails if t.get('IsMultiRegionTrail')]
301 if self.data.get('notifies'):
302 trails = [t for t in trails if t.get('SnsTopicARN')]
303 if self.data.get('running', True):
304 running = []
305 for t in list(trails):
306 t['Status'] = status = client.get_trail_status(
307 Name=t['TrailARN'])
308 if status['IsLogging'] and not status.get(
309 'LatestDeliveryError'):
310 running.append(t)
311 trails = running
312 if self.data.get('include-management-events'):
313 matched = []
314 for t in list(trails):
315 selectors = client.get_event_selectors(TrailName=t['TrailARN'])
316 if 'EventSelectors' in selectors.keys():
317 for s in selectors['EventSelectors']:
318 if s['IncludeManagementEvents'] and s['ReadWriteType'] == 'All':
319 matched.append(t)
320 elif 'AdvancedEventSelectors' in selectors.keys():
321 for s in selectors['AdvancedEventSelectors']:
322 management = False
323 readonly = False
324 for field_selector in s['FieldSelectors']:
325 if field_selector['Field'] == 'eventCategory' and \
326 'Management' in field_selector['Equals']:
327 management = True
328 elif field_selector['Field'] == 'readOnly':
329 readonly = True
330 if management and not readonly:
331 matched.append(t)
332
333 trails = matched
334 if self.data.get('log-metric-filter-pattern'):
335 client_logs = session.client('logs')
336 client_cw = session.client('cloudwatch')
337 client_sns = session.client('sns')
338 matched = []
339 pattern = self.data.get('log-metric-filter-pattern')
340 if isinstance(pattern, str):
341 vf = ValueFilter({'key': 'filterPattern', 'value': pattern})
342 else:
343 pattern.setdefault('key', 'filterPattern')
344 vf = ValueFilter(pattern)
345
346 for t in list(trails):
347 if 'CloudWatchLogsLogGroupArn' not in t.keys():
348 continue
349 log_group_name = t['CloudWatchLogsLogGroupArn'].split(':')[6]
350 try:
351 metric_filters_log_group = \
352 client_logs.describe_metric_filters(
353 logGroupName=log_group_name)['metricFilters']
354 except ClientError as e:
355 if e.response['Error']['Code'] == 'ResourceNotFoundException':
356 continue
357 filter_matched = None
358 if metric_filters_log_group:
359 for f in metric_filters_log_group:
360 if vf(f):
361 filter_matched = f
362 break
363 if not filter_matched:
364 continue
365 alarms = client_cw.describe_alarms_for_metric(
366 MetricName=filter_matched["metricTransformations"][0]["metricName"],
367 Namespace=filter_matched["metricTransformations"][0]["metricNamespace"]
368 )['MetricAlarms']
369 alarm_actions = []
370 for a in alarms:
371 alarm_actions.extend(a['AlarmActions'])
372 if not alarm_actions:
373 continue
374 alarm_actions = set(alarm_actions)
375 for a in alarm_actions:
376 try:
377 sns_topic_attributes = client_sns.get_topic_attributes(TopicArn=a)
378 sns_topic_attributes = sns_topic_attributes.get('Attributes')
379 if sns_topic_attributes.get('SubscriptionsConfirmed', '0') != '0':
380 matched.append(t)
381 except client_sns.exceptions.InvalidParameterValueException:
382 # we can ignore any exception here, the alarm action might
383 # not be an sns topic for instance
384 continue
385 trails = matched
386 if trails:
387 return []
388 return resources
389
390
391@filters.register('guard-duty')
392class GuardDutyEnabled(MultiAttrFilter):
393 """Check if the guard duty service is enabled.
394
395 This allows looking at account's detector and its associated
396 master if any.
397
398 :example:
399
400 Check to ensure guard duty is active on account and associated to a master.
401
402 .. code-block:: yaml
403
404 policies:
405 - name: guardduty-enabled
406 resource: account
407 filters:
408 - type: guard-duty
409 Detector.Status: ENABLED
410 Administrator.AccountId: "00011001"
411 Administrator.RelationshipStatus: "Enabled"
412 """
413
414 schema = {
415 'type': 'object',
416 'additionalProperties': False,
417 'properties': {
418 'type': {'enum': ['guard-duty']},
419 'match-operator': {'enum': ['or', 'and']}},
420 'patternProperties': {
421 '^Detector': {'oneOf': [{'type': 'object'}, {'type': 'string'}]},
422 '^Administrator': {'oneOf': [{'type': 'object'}, {'type': 'string'}]}},
423 }
424
425 annotation = "c7n:guard-duty"
426 permissions = (
427 'guardduty:GetAdministratorAccount',
428 'guardduty:ListDetectors',
429 'guardduty:GetDetector')
430
431 def validate(self):
432 attrs = set()
433 for k in self.data:
434 if k.startswith('Detector') or k.startswith('Master'):
435 attrs.add(k)
436 self.multi_attrs = attrs
437 return super(GuardDutyEnabled, self).validate()
438
439 def get_target(self, resource):
440 if self.annotation in resource:
441 return resource[self.annotation]
442
443 client = local_session(self.manager.session_factory).client('guardduty')
444 # detectors are singletons too.
445 detector_ids = client.list_detectors().get('DetectorIds')
446
447 if not detector_ids:
448 return None
449 else:
450 detector_id = detector_ids.pop()
451
452 detector = client.get_detector(DetectorId=detector_id)
453 detector.pop('ResponseMetadata', None)
454 admin = client.get_administrator_account(DetectorId=detector_id).get('Administrator')
455 resource[self.annotation] = r = {'Detector': detector, 'Administrator': admin}
456 return r
457
458
459@filters.register('check-config')
460class ConfigEnabled(Filter):
461 """Is config service enabled for this account
462
463 :example:
464
465 .. code-block:: yaml
466
467 policies:
468 - name: account-check-config-services
469 resource: account
470 region: us-east-1
471 filters:
472 - type: check-config
473 all-resources: true
474 global-resources: true
475 running: true
476 """
477
478 schema = type_schema(
479 'check-config', **{
480 'all-resources': {'type': 'boolean'},
481 'running': {'type': 'boolean'},
482 'global-resources': {'type': 'boolean'}})
483
484 permissions = ('config:DescribeDeliveryChannels',
485 'config:DescribeConfigurationRecorders',
486 'config:DescribeConfigurationRecorderStatus')
487
488 def process(self, resources, event=None):
489 client = local_session(
490 self.manager.session_factory).client('config')
491 channels = client.describe_delivery_channels()[
492 'DeliveryChannels']
493 recorders = client.describe_configuration_recorders()[
494 'ConfigurationRecorders']
495 resources[0]['c7n:config_recorders'] = recorders
496 resources[0]['c7n:config_channels'] = channels
497 if self.data.get('global-resources'):
498 recorders = [
499 r for r in recorders
500 if r['recordingGroup'].get('includeGlobalResourceTypes')]
501 if self.data.get('all-resources'):
502 recorders = [r for r in recorders
503 if r['recordingGroup'].get('allSupported')]
504 if self.data.get('running', True) and recorders:
505 status = {s['name']: s for
506 s in client.describe_configuration_recorder_status(
507 )['ConfigurationRecordersStatus']}
508 resources[0]['c7n:config_status'] = status
509 recorders = [r for r in recorders if status[r['name']]['recording'] and
510 status[r['name']]['lastStatus'].lower() in ('pending', 'success')]
511 if channels and recorders:
512 return []
513 return resources
514
515
516@filters.register('iam-summary')
517class IAMSummary(ValueFilter):
518 """Return annotated account resource if iam summary filter matches.
519
520 Some use cases include, detecting root api keys or mfa usage.
521
522 Example iam summary wrt to matchable fields::
523
524 {
525 "AccessKeysPerUserQuota": 2,
526 "AccountAccessKeysPresent": 0,
527 "AccountMFAEnabled": 1,
528 "AccountSigningCertificatesPresent": 0,
529 "AssumeRolePolicySizeQuota": 2048,
530 "AttachedPoliciesPerGroupQuota": 10,
531 "AttachedPoliciesPerRoleQuota": 10,
532 "AttachedPoliciesPerUserQuota": 10,
533 "GroupPolicySizeQuota": 5120,
534 "Groups": 1,
535 "GroupsPerUserQuota": 10,
536 "GroupsQuota": 100,
537 "InstanceProfiles": 0,
538 "InstanceProfilesQuota": 100,
539 "MFADevices": 3,
540 "MFADevicesInUse": 2,
541 "Policies": 3,
542 "PoliciesQuota": 1000,
543 "PolicySizeQuota": 5120,
544 "PolicyVersionsInUse": 5,
545 "PolicyVersionsInUseQuota": 10000,
546 "Providers": 0,
547 "RolePolicySizeQuota": 10240,
548 "Roles": 4,
549 "RolesQuota": 250,
550 "ServerCertificates": 0,
551 "ServerCertificatesQuota": 20,
552 "SigningCertificatesPerUserQuota": 2,
553 "UserPolicySizeQuota": 2048,
554 "Users": 5,
555 "UsersQuota": 5000,
556 "VersionsPerPolicyQuota": 5,
557 }
558
559 For example to determine if an account has either not been
560 enabled with root mfa or has root api keys.
561
562 .. code-block:: yaml
563
564 policies:
565 - name: root-keys-or-no-mfa
566 resource: account
567 filters:
568 - type: iam-summary
569 key: AccountMFAEnabled
570 value: true
571 op: eq
572 value_type: swap
573 """
574 schema = type_schema('iam-summary', rinherit=ValueFilter.schema)
575 schema_alias = False
576 permissions = ('iam:GetAccountSummary',)
577
578 def process(self, resources, event=None):
579 if not resources[0].get('c7n:iam_summary'):
580 client = local_session(
581 self.manager.session_factory).client('iam')
582 resources[0]['c7n:iam_summary'] = client.get_account_summary(
583 )['SummaryMap']
584 if self.match(resources[0]['c7n:iam_summary']):
585 return resources
586 return []
587
588
589@filters.register('access-analyzer')
590class AccessAnalyzer(ValueFilter):
591 """Check for access analyzers in an account
592
593 :example:
594
595 .. code-block:: yaml
596
597 policies:
598 - name: account-access-analyzer
599 resource: account
600 filters:
601 - type: access-analyzer
602 key: 'status'
603 value: ACTIVE
604 op: eq
605 """
606
607 schema = type_schema('access-analyzer', rinherit=ValueFilter.schema)
608 schema_alias = False
609 permissions = ('access-analyzer:ListAnalyzers',)
610 annotation_key = 'c7n:matched-analyzers'
611
612 def process(self, resources, event=None):
613 account = resources[0]
614 if not account.get(self.annotation_key):
615 client = local_session(self.manager.session_factory).client('accessanalyzer')
616 analyzers = self.manager.retry(client.list_analyzers)['analyzers']
617 else:
618 analyzers = account.get(self.annotation_key)
619
620 matched_analyzers = []
621 for analyzer in analyzers:
622 if self.match(analyzer):
623 matched_analyzers.append(analyzer)
624 account[self.annotation_key] = matched_analyzers
625 return matched_analyzers and resources or []
626
627
628@filters.register('password-policy')
629class AccountPasswordPolicy(ValueFilter):
630 """Check an account's password policy.
631
632 Note that on top of the default password policy fields, we also add an extra key,
633 PasswordPolicyConfigured which will be set to true or false to signify if the given
634 account has attempted to set a policy at all.
635
636 :example:
637
638 .. code-block:: yaml
639
640 policies:
641 - name: password-policy-check
642 resource: account
643 region: us-east-1
644 filters:
645 - type: password-policy
646 key: MinimumPasswordLength
647 value: 10
648 op: ge
649 - type: password-policy
650 key: RequireSymbols
651 value: true
652 """
653 schema = type_schema('password-policy', rinherit=ValueFilter.schema)
654 schema_alias = False
655 permissions = ('iam:GetAccountPasswordPolicy',)
656
657 def process(self, resources, event=None):
658 account = resources[0]
659 if not account.get('c7n:password_policy'):
660 client = local_session(self.manager.session_factory).client('iam')
661 policy = {}
662 try:
663 policy = client.get_account_password_policy().get('PasswordPolicy', {})
664 policy['PasswordPolicyConfigured'] = True
665 except ClientError as e:
666 if e.response['Error']['Code'] == 'NoSuchEntity':
667 policy['PasswordPolicyConfigured'] = False
668 else:
669 raise
670 account['c7n:password_policy'] = policy
671 if self.match(account['c7n:password_policy']):
672 return resources
673 return []
674
675
676@actions.register('set-password-policy')
677class SetAccountPasswordPolicy(BaseAction):
678 """Set an account's password policy.
679
680 This only changes the policy for the items provided.
681 If this is the first time setting a password policy and an item is not provided it will be
682 set to the defaults defined in the boto docs for IAM.Client.update_account_password_policy
683
684 :example:
685
686 .. code-block:: yaml
687
688 policies:
689 - name: set-account-password-policy
690 resource: account
691 filters:
692 - not:
693 - type: password-policy
694 key: MinimumPasswordLength
695 value: 10
696 op: ge
697 actions:
698 - type: set-password-policy
699 policy:
700 MinimumPasswordLength: 20
701 """
702 schema = type_schema(
703 'set-password-policy',
704 policy={
705 'type': 'object'
706 })
707 shape = 'UpdateAccountPasswordPolicyRequest'
708 service = 'iam'
709 permissions = ('iam:GetAccountPasswordPolicy', 'iam:UpdateAccountPasswordPolicy')
710
711 def validate(self):
712 return shape_validate(
713 self.data.get('policy', {}),
714 self.shape,
715 self.service)
716
717 def process(self, resources):
718 client = local_session(self.manager.session_factory).client('iam')
719 account = resources[0]
720 if account.get('c7n:password_policy'):
721 config = account['c7n:password_policy']
722 else:
723 try:
724 config = client.get_account_password_policy().get('PasswordPolicy')
725 except client.exceptions.NoSuchEntityException:
726 config = {}
727 params = dict(self.data['policy'])
728 config.update(params)
729 config = {k: v for (k, v) in config.items() if k not in ('ExpirePasswords',
730 'PasswordPolicyConfigured')}
731 client.update_account_password_policy(**config)
732
733
734@filters.register('service-limit')
735class ServiceLimit(Filter):
736 """Check if account's service limits are past a given threshold.
737
738 Supported limits are per trusted advisor, which is variable based
739 on usage in the account and support level enabled on the account.
740
741 The `names` attribute lets you filter which checks to query limits
742 about. This is a case-insensitive globbing match on a check name.
743 You can specify a name exactly or use globbing wildcards like `VPC*`.
744
745 The names are exactly what's shown on the trusted advisor page:
746
747 https://console.aws.amazon.com/trustedadvisor/home#/category/service-limits
748
749 or via the awscli:
750
751 aws --region us-east-1 support describe-trusted-advisor-checks --language en \
752 --query 'checks[?category==`service_limits`].[name]' --output text
753
754 While you can target individual checks via the `names` attribute, and
755 that should be the preferred method, the following are provided for
756 backward compatibility with the old style of checks:
757
758 - `services`
759
760 The resulting limit's `service` field must match one of these.
761 These are case-insensitive globbing matches.
762
763 Note: If you haven't specified any `names` to filter, then
764 these service names are used as a case-insensitive prefix match on
765 the check name. This helps limit the number of API calls we need
766 to make.
767
768 - `limits`
769
770 The resulting limit's `Limit Name` field must match one of these.
771 These are case-insensitive globbing matches.
772
773 Some example names and their corresponding service and limit names:
774
775 Check Name Service Limit Name
776 ---------------------------------- -------------- ---------------------------------
777 Auto Scaling Groups AutoScaling Auto Scaling groups
778 Auto Scaling Launch Configurations AutoScaling Launch configurations
779 CloudFormation Stacks CloudFormation Stacks
780 ELB Application Load Balancers ELB Active Application Load Balancers
781 ELB Classic Load Balancers ELB Active load balancers
782 ELB Network Load Balancers ELB Active Network Load Balancers
783 VPC VPC VPCs
784 VPC Elastic IP Address VPC VPC Elastic IP addresses (EIPs)
785 VPC Internet Gateways VPC Internet gateways
786
787 Note: Some service limits checks are being migrated to service quotas,
788 which is expected to largely replace service limit checks in trusted
789 advisor. In this case, some of these checks have no results.
790
791 :example:
792
793 .. code-block:: yaml
794
795 policies:
796 - name: specific-account-service-limits
797 resource: account
798 filters:
799 - type: service-limit
800 names:
801 - IAM Policies
802 - IAM Roles
803 - "VPC*"
804 threshold: 1.0
805
806 - name: increase-account-service-limits
807 resource: account
808 filters:
809 - type: service-limit
810 services:
811 - EC2
812 threshold: 1.0
813
814 - name: specify-region-for-global-service
815 region: us-east-1
816 resource: account
817 filters:
818 - type: service-limit
819 services:
820 - IAM
821 limits:
822 - Roles
823 """
824
825 schema = type_schema(
826 'service-limit',
827 threshold={'type': 'number'},
828 refresh_period={'type': 'integer',
829 'title': 'how long should a check result be considered fresh'},
830 names={'type': 'array', 'items': {'type': 'string'}},
831 limits={'type': 'array', 'items': {'type': 'string'}},
832 services={'type': 'array', 'items': {
833 'enum': ['AutoScaling', 'CloudFormation',
834 'DynamoDB', 'EBS', 'EC2', 'ELB',
835 'IAM', 'RDS', 'Route53', 'SES', 'VPC']}})
836
837 permissions = ('support:DescribeTrustedAdvisorCheckRefreshStatuses',
838 'support:DescribeTrustedAdvisorCheckResult',
839 'support:DescribeTrustedAdvisorChecks',
840 'support:RefreshTrustedAdvisorCheck')
841 deprecated_check_ids = ['eW7HH0l7J9']
842 check_limit = ('region', 'service', 'check', 'limit', 'extant', 'color')
843
844 # When doing a refresh, how long to wait for the check to become ready.
845 # Max wait here is 5 * 10 ~ 50 seconds.
846 poll_interval = 5
847 poll_max_intervals = 10
848 global_services = {'IAM'}
849
850 def validate(self):
851 region = self.manager.data.get('region', '')
852 if len(self.global_services.intersection(self.data.get('services', []))):
853 if region != 'us-east-1':
854 raise PolicyValidationError(
855 "Global services: %s must be targeted in us-east-1 on the policy"
856 % ', '.join(self.global_services))
857 return self
858
859 @classmethod
860 def get_check_result(cls, client, check_id):
861 checks = client.describe_trusted_advisor_check_result(
862 checkId=check_id, language='en')['result']
863
864 # Check status and if necessary refresh checks
865 if checks['status'] == 'not_available':
866 try:
867 client.refresh_trusted_advisor_check(checkId=check_id)
868 except ClientError as e:
869 if e.response['Error']['Code'] == 'InvalidParameterValueException':
870 cls.log.warning("InvalidParameterValueException: %s",
871 e.response['Error']['Message'])
872 return
873
874 for _ in range(cls.poll_max_intervals):
875 time.sleep(cls.poll_interval)
876 refresh_response = client.describe_trusted_advisor_check_refresh_statuses(
877 checkIds=[check_id])
878 if refresh_response['statuses'][0]['status'] == 'success':
879 checks = client.describe_trusted_advisor_check_result(
880 checkId=check_id, language='en')['result']
881 break
882 return checks
883
884 def get_available_checks(self, client, category='service_limits'):
885 checks = client.describe_trusted_advisor_checks(language='en')
886 return [c for c in checks['checks']
887 if c['category'] == category and
888 c['id'] not in self.deprecated_check_ids]
889
890 def match_patterns_to_value(self, patterns, value):
891 for p in patterns:
892 if fnmatch(value.lower(), p.lower()):
893 return True
894 return False
895
896 def should_process(self, name):
897 # if names specified, limit to these names
898 patterns = self.data.get('names')
899 if patterns:
900 return self.match_patterns_to_value(patterns, name)
901
902 # otherwise, if services specified, limit to those prefixes
903 services = self.data.get('services')
904 if services:
905 patterns = ["{}*".format(i) for i in services]
906 return self.match_patterns_to_value(patterns, name.replace(' ', ''))
907
908 return True
909
910 def process(self, resources, event=None):
911 support_region = get_support_region(self.manager)
912 client = local_session(self.manager.session_factory).client(
913 'support', region_name=support_region)
914 checks = self.get_available_checks(client)
915 exceeded = []
916 for check in checks:
917 if not self.should_process(check['name']):
918 continue
919 matched = self.process_check(client, check, resources, event)
920 if matched:
921 for m in matched:
922 m['check_id'] = check['id']
923 m['name'] = check['name']
924 exceeded.extend(matched)
925 if exceeded:
926 resources[0]['c7n:ServiceLimitsExceeded'] = exceeded
927 return resources
928 return []
929
930 def process_check(self, client, check, resources, event=None):
931 region = self.manager.config.region
932 results = self.get_check_result(client, check['id'])
933
934 if results is None or 'flaggedResources' not in results:
935 return []
936
937 # trim to only results for this region
938 results['flaggedResources'] = [
939 r
940 for r in results.get('flaggedResources', [])
941 if r['metadata'][0] == region or (r['metadata'][0] == '-' and region == 'us-east-1')
942 ]
943
944 # save all raw limit results to the account resource
945 if 'c7n:ServiceLimits' not in resources[0]:
946 resources[0]['c7n:ServiceLimits'] = []
947 resources[0]['c7n:ServiceLimits'].append(results)
948
949 # check if we need to refresh the check for next time
950 delta = datetime.timedelta(self.data.get('refresh_period', 1))
951 check_date = parse_date(results['timestamp'])
952 if datetime.datetime.now(tz=tzutc()) - delta > check_date:
953 try:
954 client.refresh_trusted_advisor_check(checkId=check['id'])
955 except ClientError as e:
956 if e.response['Error']['Code'] == 'InvalidParameterValueException':
957 self.log.warning("InvalidParameterValueException: %s",
958 e.response['Error']['Message'])
959 return
960
961 services = self.data.get('services')
962 limits = self.data.get('limits')
963 threshold = self.data.get('threshold')
964 exceeded = []
965
966 for resource in results['flaggedResources']:
967 if threshold is None and resource['status'] == 'ok':
968 continue
969 limit = dict(zip(self.check_limit, resource['metadata']))
970 if services and not self.match_patterns_to_value(services, limit['service']):
971 continue
972 if limits and not self.match_patterns_to_value(limits, limit['check']):
973 continue
974 limit['status'] = resource['status']
975 limit['percentage'] = (
976 float(limit['extant'] or 0) / float(limit['limit']) * 100
977 )
978 if threshold and limit['percentage'] < threshold:
979 continue
980 exceeded.append(limit)
981 return exceeded
982
983
984@actions.register('request-limit-increase')
985class RequestLimitIncrease(BaseAction):
986 r"""File support ticket to raise limit.
987
988 :Example:
989
990 .. code-block:: yaml
991
992 policies:
993 - name: raise-account-service-limits
994 resource: account
995 filters:
996 - type: service-limit
997 services:
998 - EBS
999 limits:
1000 - Provisioned IOPS (SSD) storage (GiB)
1001 threshold: 60.5
1002 actions:
1003 - type: request-limit-increase
1004 notify: [email, email2]
1005 ## You can use one of either percent-increase or an amount-increase.
1006 percent-increase: 50
1007 message: "Please raise the below account limit(s); \n {limits}"
1008 """
1009
1010 schema = {
1011 'type': 'object',
1012 'additionalProperties': False,
1013 'properties': {
1014 'type': {'enum': ['request-limit-increase']},
1015 'percent-increase': {'type': 'number', 'minimum': 1},
1016 'amount-increase': {'type': 'number', 'minimum': 1},
1017 'minimum-increase': {'type': 'number', 'minimum': 1},
1018 'subject': {'type': 'string'},
1019 'message': {'type': 'string'},
1020 'notify': {'type': 'array', 'items': {'type': 'string'}},
1021 'severity': {'type': 'string', 'enum': ['urgent', 'high', 'normal', 'low']}
1022 },
1023 'oneOf': [
1024 {'required': ['type', 'percent-increase']},
1025 {'required': ['type', 'amount-increase']}
1026 ]
1027 }
1028
1029 permissions = ('support:CreateCase',)
1030
1031 default_subject = '[Account:{account}]Raise the following limit(s) of {service} in {region}'
1032 default_template = 'Please raise the below account limit(s); \n {limits}'
1033 default_severity = 'normal'
1034
1035 service_code_mapping = {
1036 'AutoScaling': 'auto-scaling',
1037 'CloudFormation': 'aws-cloudformation',
1038 'DynamoDB': 'amazon-dynamodb',
1039 'EBS': 'amazon-elastic-block-store',
1040 'EC2': 'amazon-elastic-compute-cloud-linux',
1041 'ELB': 'elastic-load-balancing',
1042 'IAM': 'aws-identity-and-access-management',
1043 'Kinesis': 'amazon-kinesis',
1044 'RDS': 'amazon-relational-database-service-aurora',
1045 'Route53': 'amazon-route53',
1046 'SES': 'amazon-simple-email-service',
1047 'VPC': 'amazon-virtual-private-cloud',
1048 }
1049
1050 def process(self, resources):
1051 support_region = get_support_region(self.manager)
1052 client = local_session(self.manager.session_factory).client(
1053 'support', region_name=support_region)
1054 account_id = self.manager.config.account_id
1055 service_map = {}
1056 region_map = {}
1057 limit_exceeded = resources[0].get('c7n:ServiceLimitsExceeded', [])
1058 percent_increase = self.data.get('percent-increase')
1059 amount_increase = self.data.get('amount-increase')
1060 minimum_increase = self.data.get('minimum-increase', 1)
1061
1062 for s in limit_exceeded:
1063 current_limit = int(s['limit'])
1064 if percent_increase:
1065 increase_by = current_limit * float(percent_increase) / 100
1066 increase_by = max(increase_by, minimum_increase)
1067 else:
1068 increase_by = amount_increase
1069 increase_by = round(increase_by)
1070 msg = '\nIncrease %s by %d in %s \n\t Current Limit: %s\n\t Current Usage: %s\n\t ' \
1071 'Set New Limit to: %d' % (
1072 s['check'], increase_by, s['region'], s['limit'], s['extant'],
1073 (current_limit + increase_by))
1074 service_map.setdefault(s['service'], []).append(msg)
1075 region_map.setdefault(s['service'], s['region'])
1076
1077 for service in service_map:
1078 subject = self.data.get('subject', self.default_subject).format(
1079 service=service, region=region_map[service], account=account_id)
1080 service_code = self.service_code_mapping.get(service)
1081 body = self.data.get('message', self.default_template)
1082 body = body.format(**{
1083 'service': service,
1084 'limits': '\n\t'.join(service_map[service]),
1085 })
1086 client.create_case(
1087 subject=subject,
1088 communicationBody=body,
1089 serviceCode=service_code,
1090 categoryCode='general-guidance',
1091 severityCode=self.data.get('severity', self.default_severity),
1092 ccEmailAddresses=self.data.get('notify', []))
1093
1094
1095def cloudtrail_policy(original, bucket_name, account_id, bucket_region):
1096 '''add CloudTrail permissions to an S3 policy, preserving existing'''
1097 ct_actions = [
1098 {
1099 'Action': 's3:GetBucketAcl',
1100 'Effect': 'Allow',
1101 'Principal': {'Service': 'cloudtrail.amazonaws.com'},
1102 'Resource': generate_arn(
1103 service='s3', resource=bucket_name, region=bucket_region),
1104 'Sid': 'AWSCloudTrailAclCheck20150319',
1105 },
1106 {
1107 'Action': 's3:PutObject',
1108 'Condition': {
1109 'StringEquals':
1110 {'s3:x-amz-acl': 'bucket-owner-full-control'},
1111 },
1112 'Effect': 'Allow',
1113 'Principal': {'Service': 'cloudtrail.amazonaws.com'},
1114 'Resource': generate_arn(
1115 service='s3', resource=bucket_name, region=bucket_region),
1116 'Sid': 'AWSCloudTrailWrite20150319',
1117 },
1118 ]
1119 # parse original policy
1120 if original is None:
1121 policy = {
1122 'Statement': [],
1123 'Version': '2012-10-17',
1124 }
1125 else:
1126 policy = json.loads(original['Policy'])
1127 original_actions = [a.get('Action') for a in policy['Statement']]
1128 for cta in ct_actions:
1129 if cta['Action'] not in original_actions:
1130 policy['Statement'].append(cta)
1131 return json.dumps(policy)
1132
1133
1134# AWS Account doesn't participate in events (not based on query resource manager)
1135# so the event subscriber used by postfinding to register doesn't apply, manually
1136# register it.
1137Account.action_registry.register('post-finding', OtherResourcePostFinding)
1138
1139
1140@actions.register('enable-cloudtrail')
1141class EnableTrail(BaseAction):
1142 """Enables logging on the trail(s) named in the policy
1143
1144 :Example:
1145
1146 .. code-block:: yaml
1147
1148 policies:
1149 - name: trail-test
1150 description: Ensure CloudTrail logging is enabled
1151 resource: account
1152 actions:
1153 - type: enable-cloudtrail
1154 trail: mytrail
1155 bucket: trails
1156 """
1157
1158 permissions = (
1159 'cloudtrail:CreateTrail',
1160 'cloudtrail:DescribeTrails',
1161 'cloudtrail:GetTrailStatus',
1162 'cloudtrail:StartLogging',
1163 'cloudtrail:UpdateTrail',
1164 's3:CreateBucket',
1165 's3:GetBucketPolicy',
1166 's3:PutBucketPolicy',
1167 )
1168 schema = type_schema(
1169 'enable-cloudtrail',
1170 **{
1171 'trail': {'type': 'string'},
1172 'bucket': {'type': 'string'},
1173 'bucket-region': {'type': 'string'},
1174 'multi-region': {'type': 'boolean'},
1175 'global-events': {'type': 'boolean'},
1176 'notify': {'type': 'string'},
1177 'file-digest': {'type': 'boolean'},
1178 'kms': {'type': 'boolean'},
1179 'kms-key': {'type': 'string'},
1180 'required': ('bucket',),
1181 }
1182 )
1183
1184 def process(self, accounts):
1185 """Create or enable CloudTrail"""
1186 session = local_session(self.manager.session_factory)
1187 client = session.client('cloudtrail')
1188 bucket_name = self.data['bucket']
1189 bucket_region = self.data.get('bucket-region', 'us-east-1')
1190 trail_name = self.data.get('trail', 'default-trail')
1191 multi_region = self.data.get('multi-region', True)
1192 global_events = self.data.get('global-events', True)
1193 notify = self.data.get('notify', '')
1194 file_digest = self.data.get('file-digest', False)
1195 kms = self.data.get('kms', False)
1196 kms_key = self.data.get('kms-key', '')
1197
1198 s3client = session.client('s3', region_name=bucket_region)
1199 try:
1200 s3client.create_bucket(
1201 Bucket=bucket_name,
1202 CreateBucketConfiguration={'LocationConstraint': bucket_region}
1203 )
1204 except ClientError as ce:
1205 if not ('Error' in ce.response and
1206 ce.response['Error']['Code'] == 'BucketAlreadyOwnedByYou'):
1207 raise ce
1208
1209 try:
1210 current_policy = s3client.get_bucket_policy(Bucket=bucket_name)
1211 except ClientError:
1212 current_policy = None
1213
1214 policy_json = cloudtrail_policy(
1215 current_policy, bucket_name,
1216 self.manager.config.account_id, bucket_region)
1217
1218 s3client.put_bucket_policy(Bucket=bucket_name, Policy=policy_json)
1219 trails = client.describe_trails().get('trailList', ())
1220 if trail_name not in [t.get('Name') for t in trails]:
1221 new_trail = client.create_trail(
1222 Name=trail_name,
1223 S3BucketName=bucket_name,
1224 )
1225 if new_trail:
1226 trails.append(new_trail)
1227 # the loop below will configure the new trail
1228 for trail in trails:
1229 if trail.get('Name') != trail_name:
1230 continue
1231 # enable
1232 arn = trail['TrailARN']
1233 status = client.get_trail_status(Name=arn)
1234 if not status['IsLogging']:
1235 client.start_logging(Name=arn)
1236 # apply configuration changes (if any)
1237 update_args = {}
1238 if multi_region != trail.get('IsMultiRegionTrail'):
1239 update_args['IsMultiRegionTrail'] = multi_region
1240 if global_events != trail.get('IncludeGlobalServiceEvents'):
1241 update_args['IncludeGlobalServiceEvents'] = global_events
1242 if notify != trail.get('SNSTopicArn'):
1243 update_args['SnsTopicName'] = notify
1244 if file_digest != trail.get('LogFileValidationEnabled'):
1245 update_args['EnableLogFileValidation'] = file_digest
1246 if kms_key != trail.get('KmsKeyId'):
1247 if not kms and 'KmsKeyId' in trail:
1248 kms_key = ''
1249 update_args['KmsKeyId'] = kms_key
1250 if update_args:
1251 update_args['Name'] = trail_name
1252 client.update_trail(**update_args)
1253
1254
1255@filters.register('has-virtual-mfa')
1256class HasVirtualMFA(Filter):
1257 """Is the account configured with a virtual MFA device?
1258
1259 :example:
1260
1261 .. code-block:: yaml
1262
1263 policies:
1264 - name: account-with-virtual-mfa
1265 resource: account
1266 region: us-east-1
1267 filters:
1268 - type: has-virtual-mfa
1269 value: true
1270 """
1271
1272 schema = type_schema('has-virtual-mfa', **{'value': {'type': 'boolean'}})
1273
1274 permissions = ('iam:ListVirtualMFADevices',)
1275
1276 def mfa_belongs_to_root_account(self, mfa):
1277 mfa_user = mfa.get('User', {}).get('Arn', '').split(':')[-1]
1278 return mfa_user == 'root'
1279
1280 def account_has_virtual_mfa(self, account):
1281 if not account.get('c7n:VirtualMFADevices'):
1282 client = local_session(self.manager.session_factory).client('iam')
1283 paginator = client.get_paginator('list_virtual_mfa_devices')
1284 raw_list = paginator.paginate().build_full_result()['VirtualMFADevices']
1285 account['c7n:VirtualMFADevices'] = list(filter(
1286 self.mfa_belongs_to_root_account, raw_list))
1287 expect_virtual_mfa = self.data.get('value', True)
1288 has_virtual_mfa = any(account['c7n:VirtualMFADevices'])
1289 return expect_virtual_mfa == has_virtual_mfa
1290
1291 def process(self, resources, event=None):
1292 return list(filter(self.account_has_virtual_mfa, resources))
1293
1294
1295@actions.register('enable-data-events')
1296class EnableDataEvents(BaseAction):
1297 """Ensure all buckets in account are setup to log data events.
1298
1299 Note this works via a single trail for data events per
1300 https://aws.amazon.com/about-aws/whats-new/2017/09/aws-cloudtrail-enables-option-to-add-all-amazon-s3-buckets-to-data-events/
1301
1302 This trail should NOT be used for api management events, the
1303 configuration here is soley for data events. If directed to create
1304 a trail this will do so without management events.
1305
1306 :example:
1307
1308 .. code-block:: yaml
1309
1310 policies:
1311 - name: s3-enable-data-events-logging
1312 resource: account
1313 actions:
1314 - type: enable-data-events
1315 data-trail:
1316 name: s3-events
1317 multi-region: us-east-1
1318 """
1319
1320 schema = type_schema(
1321 'enable-data-events', required=['data-trail'], **{
1322 'data-trail': {
1323 'type': 'object',
1324 'additionalProperties': False,
1325 'required': ['name'],
1326 'properties': {
1327 'create': {
1328 'title': 'Should we create trail if needed for events?',
1329 'type': 'boolean'},
1330 'type': {'enum': ['ReadOnly', 'WriteOnly', 'All']},
1331 'name': {
1332 'title': 'The name of the event trail',
1333 'type': 'string'},
1334 'topic': {
1335 'title': 'If creating, the sns topic for the trail to send updates',
1336 'type': 'string'},
1337 's3-bucket': {
1338 'title': 'If creating, the bucket to store trail event data',
1339 'type': 'string'},
1340 's3-prefix': {'type': 'string'},
1341 'key-id': {
1342 'title': 'If creating, Enable kms on the trail',
1343 'type': 'string'},
1344 # region that we're aggregating via trails.
1345 'multi-region': {
1346 'title': 'If creating, use this region for all data trails',
1347 'type': 'string'}}}})
1348
1349 def validate(self):
1350 if self.data['data-trail'].get('create'):
1351 if 's3-bucket' not in self.data['data-trail']:
1352 raise PolicyValidationError(
1353 "If creating data trails, an s3-bucket is required on %s" % (
1354 self.manager.data))
1355 return self
1356
1357 def get_permissions(self):
1358 perms = [
1359 'cloudtrail:DescribeTrails',
1360 'cloudtrail:GetEventSelectors',
1361 'cloudtrail:PutEventSelectors']
1362
1363 if self.data.get('data-trail', {}).get('create'):
1364 perms.extend([
1365 'cloudtrail:CreateTrail', 'cloudtrail:StartLogging'])
1366 return perms
1367
1368 def add_data_trail(self, client, trail_cfg):
1369 if not trail_cfg.get('create'):
1370 raise ValueError(
1371 "s3 data event trail missing and not configured to create")
1372 params = dict(
1373 Name=trail_cfg['name'],
1374 S3BucketName=trail_cfg['s3-bucket'],
1375 EnableLogFileValidation=True)
1376
1377 if 'key-id' in trail_cfg:
1378 params['KmsKeyId'] = trail_cfg['key-id']
1379 if 's3-prefix' in trail_cfg:
1380 params['S3KeyPrefix'] = trail_cfg['s3-prefix']
1381 if 'topic' in trail_cfg:
1382 params['SnsTopicName'] = trail_cfg['topic']
1383 if 'multi-region' in trail_cfg:
1384 params['IsMultiRegionTrail'] = True
1385
1386 client.create_trail(**params)
1387 return {'Name': trail_cfg['name']}
1388
1389 def process(self, resources):
1390 session = local_session(self.manager.session_factory)
1391 region = self.data['data-trail'].get('multi-region')
1392
1393 if region:
1394 client = session.client('cloudtrail', region_name=region)
1395 else:
1396 client = session.client('cloudtrail')
1397
1398 added = False
1399 tconfig = self.data['data-trail']
1400 trails = client.describe_trails(
1401 trailNameList=[tconfig['name']]).get('trailList', ())
1402 if not trails:
1403 trail = self.add_data_trail(client, tconfig)
1404 added = True
1405 else:
1406 trail = trails[0]
1407
1408 events = client.get_event_selectors(
1409 TrailName=trail['Name']).get('EventSelectors', [])
1410
1411 for e in events:
1412 found = False
1413 if not e.get('DataResources'):
1414 continue
1415 for data_events in e['DataResources']:
1416 if data_events['Type'] != 'AWS::S3::Object':
1417 continue
1418 for b in data_events['Values']:
1419 if b.rsplit(':')[-1].strip('/') == '':
1420 found = True
1421 break
1422 if found:
1423 resources[0]['c7n_data_trail'] = trail
1424 return
1425
1426 # Opinionated choice, separate api and data events.
1427 event_count = len(events)
1428 events = [e for e in events if not e.get('IncludeManagementEvents')]
1429 if len(events) != event_count:
1430 self.log.warning("removing api trail from data trail")
1431
1432 # future proof'd for other data events, for s3 this trail
1433 # encompasses all the buckets in the account.
1434
1435 events.append({
1436 'IncludeManagementEvents': False,
1437 'ReadWriteType': tconfig.get('type', 'All'),
1438 'DataResources': [{
1439 'Type': 'AWS::S3::Object',
1440 'Values': ['arn:aws:s3:::']}]})
1441 client.put_event_selectors(
1442 TrailName=trail['Name'],
1443 EventSelectors=events)
1444
1445 if added:
1446 client.start_logging(Name=tconfig['name'])
1447
1448 resources[0]['c7n_data_trail'] = trail
1449
1450
1451@filters.register('shield-enabled')
1452class ShieldEnabled(Filter):
1453
1454 permissions = ('shield:DescribeSubscription',)
1455
1456 schema = type_schema(
1457 'shield-enabled',
1458 state={'type': 'boolean'})
1459
1460 def process(self, resources, event=None):
1461 state = self.data.get('state', False)
1462 client = local_session(self.manager.session_factory).client('shield')
1463 try:
1464 subscription = client.describe_subscription().get(
1465 'Subscription', None)
1466 except ClientError as e:
1467 if e.response['Error']['Code'] != 'ResourceNotFoundException':
1468 raise
1469 subscription = None
1470
1471 resources[0]['c7n:ShieldSubscription'] = subscription
1472 if state and subscription:
1473 return resources
1474 elif not state and not subscription:
1475 return resources
1476 return []
1477
1478
1479@actions.register('set-shield-advanced')
1480class SetShieldAdvanced(BaseAction):
1481 """Enable/disable Shield Advanced on an account."""
1482
1483 permissions = (
1484 'shield:CreateSubscription', 'shield:DeleteSubscription')
1485
1486 schema = type_schema(
1487 'set-shield-advanced',
1488 state={'type': 'boolean'})
1489
1490 def process(self, resources):
1491 client = local_session(self.manager.session_factory).client('shield')
1492 state = self.data.get('state', True)
1493
1494 if state:
1495 client.create_subscription()
1496 else:
1497 try:
1498 client.delete_subscription()
1499 except ClientError as e:
1500 if e.response['Error']['Code'] == 'ResourceNotFoundException':
1501 return
1502 raise
1503
1504
1505@filters.register('xray-encrypt-key')
1506class XrayEncrypted(Filter):
1507 """Determine if xray is encrypted.
1508
1509 :example:
1510
1511 .. code-block:: yaml
1512
1513 policies:
1514 - name: xray-encrypt-with-default
1515 resource: aws.account
1516 filters:
1517 - type: xray-encrypt-key
1518 key: default
1519 - name: xray-encrypt-with-kms
1520 resource: aws.account
1521 filters:
1522 - type: xray-encrypt-key
1523 key: kms
1524 - name: xray-encrypt-with-specific-key
1525 resource: aws.account
1526 filters:
1527 - type: xray-encrypt-key
1528 key: alias/my-alias or arn or keyid
1529 """
1530
1531 permissions = ('xray:GetEncryptionConfig',)
1532 schema = type_schema(
1533 'xray-encrypt-key',
1534 required=['key'],
1535 key={'type': 'string'}
1536 )
1537
1538 def process(self, resources, event=None):
1539 client = self.manager.session_factory().client('xray')
1540 gec_result = client.get_encryption_config()['EncryptionConfig']
1541 resources[0]['c7n:XrayEncryptionConfig'] = gec_result
1542
1543 k = self.data.get('key')
1544 if k not in ['default', 'kms']:
1545 kmsclient = self.manager.session_factory().client('kms')
1546 keyid = kmsclient.describe_key(KeyId=k)['KeyMetadata']['Arn']
1547 rc = resources if (gec_result['KeyId'] == keyid) else []
1548 else:
1549 kv = 'KMS' if self.data.get('key') == 'kms' else 'NONE'
1550 rc = resources if (gec_result['Type'] == kv) else []
1551 return rc
1552
1553
1554@actions.register('set-xray-encrypt')
1555class SetXrayEncryption(BaseAction):
1556 """Enable specific xray encryption.
1557
1558 :example:
1559
1560 .. code-block:: yaml
1561
1562 policies:
1563 - name: xray-default-encrypt
1564 resource: aws.account
1565 actions:
1566 - type: set-xray-encrypt
1567 key: default
1568 - name: xray-kms-encrypt
1569 resource: aws.account
1570 actions:
1571 - type: set-xray-encrypt
1572 key: alias/some/alias/key
1573 """
1574
1575 permissions = ('xray:PutEncryptionConfig',)
1576 schema = type_schema(
1577 'set-xray-encrypt',
1578 required=['key'],
1579 key={'type': 'string'}
1580 )
1581
1582 def process(self, resources):
1583 client = local_session(self.manager.session_factory).client('xray')
1584 key = self.data.get('key')
1585 req = {'Type': 'NONE'} if key == 'default' else {'Type': 'KMS', 'KeyId': key}
1586 client.put_encryption_config(**req)
1587
1588
1589@filters.register('default-ebs-encryption')
1590class EbsEncryption(Filter):
1591 """Filter an account by its ebs encryption status.
1592
1593 By default for key we match on the alias name for a key.
1594
1595 :example:
1596
1597 .. code-block:: yaml
1598
1599 policies:
1600 - name: check-default-ebs-encryption
1601 resource: aws.account
1602 filters:
1603 - type: default-ebs-encryption
1604 key: "alias/aws/ebs"
1605 state: true
1606
1607 It is also possible to match on specific key attributes (tags, origin)
1608
1609 :example:
1610
1611 .. code-block:: yaml
1612
1613 policies:
1614 - name: check-ebs-encryption-key-origin
1615 resource: aws.account
1616 filters:
1617 - type: default-ebs-encryption
1618 key:
1619 type: value
1620 key: Origin
1621 value: AWS_KMS
1622 state: true
1623 """
1624 permissions = ('ec2:GetEbsEncryptionByDefault',)
1625 schema = type_schema(
1626 'default-ebs-encryption',
1627 state={'type': 'boolean'},
1628 key={'oneOf': [
1629 {'$ref': '#/definitions/filters/value'},
1630 {'type': 'string'}]})
1631
1632 def process(self, resources, event=None):
1633 state = self.data.get('state', False)
1634 client = local_session(self.manager.session_factory).client('ec2')
1635 account_state = client.get_ebs_encryption_by_default().get(
1636 'EbsEncryptionByDefault')
1637 if account_state != state:
1638 return []
1639 if state and 'key' in self.data:
1640 vfd = (isinstance(self.data['key'], dict) and
1641 self.data['key'] or {'c7n:AliasName': self.data['key']})
1642 vf = KmsRelatedFilter(vfd, self.manager)
1643 vf.RelatedIdsExpression = 'KmsKeyId'
1644 vf.annotate = False
1645 key = client.get_ebs_default_kms_key_id().get('KmsKeyId')
1646 if not vf.process([{'KmsKeyId': key}]):
1647 return []
1648 return resources
1649
1650
1651@actions.register('set-ebs-encryption')
1652class SetEbsEncryption(BaseAction):
1653 """Set AWS EBS default encryption on an account
1654
1655 :example:
1656
1657 .. code-block:: yaml
1658
1659 policies:
1660 - name: set-default-ebs-encryption
1661 resource: aws.account
1662 filters:
1663 - type: default-ebs-encryption
1664 state: false
1665 actions:
1666 - type: set-ebs-encryption
1667 state: true
1668 key: alias/aws/ebs
1669 """
1670 permissions = ('ec2:EnableEbsEncryptionByDefault',
1671 'ec2:DisableEbsEncryptionByDefault')
1672
1673 schema = type_schema(
1674 'set-ebs-encryption',
1675 state={'type': 'boolean'},
1676 key={'type': 'string'})
1677
1678 def process(self, resources):
1679 client = local_session(
1680 self.manager.session_factory).client('ec2')
1681 state = self.data.get('state')
1682 key = self.data.get('key')
1683 if state:
1684 client.enable_ebs_encryption_by_default()
1685 else:
1686 client.disable_ebs_encryption_by_default()
1687
1688 if state and key:
1689 client.modify_ebs_default_kms_key_id(
1690 KmsKeyId=self.data['key'])
1691
1692
1693@filters.register('s3-public-block')
1694class S3PublicBlock(ValueFilter):
1695 """Check for s3 public blocks on an account.
1696
1697 https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html
1698 """
1699
1700 annotation_key = 'c7n:s3-public-block'
1701 annotate = False # no annotation from value filter
1702 schema = type_schema('s3-public-block', rinherit=ValueFilter.schema)
1703 schema_alias = False
1704 permissions = ('s3:GetAccountPublicAccessBlock',)
1705
1706 def process(self, resources, event=None):
1707 self.augment([r for r in resources if self.annotation_key not in r])
1708 return super(S3PublicBlock, self).process(resources, event)
1709
1710 def augment(self, resources):
1711 client = local_session(self.manager.session_factory).client('s3control')
1712 for r in resources:
1713 try:
1714 r[self.annotation_key] = client.get_public_access_block(
1715 AccountId=r['account_id']).get('PublicAccessBlockConfiguration', {})
1716 except client.exceptions.NoSuchPublicAccessBlockConfiguration:
1717 r[self.annotation_key] = {}
1718
1719 def __call__(self, r):
1720 return super(S3PublicBlock, self).__call__(r[self.annotation_key])
1721
1722
1723@actions.register('set-s3-public-block')
1724class SetS3PublicBlock(BaseAction):
1725 """Configure S3 Public Access Block on an account.
1726
1727 All public access block attributes can be set. If not specified they are merged
1728 with the extant configuration.
1729
1730 https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html
1731
1732 :example:
1733
1734 .. yaml:
1735
1736 policies:
1737 - name: restrict-public-buckets
1738 resource: aws.account
1739 filters:
1740 - not:
1741 - type: s3-public-block
1742 key: RestrictPublicBuckets
1743 value: true
1744 actions:
1745 - type: set-s3-public-block
1746 RestrictPublicBuckets: true
1747
1748 """
1749 schema = type_schema(
1750 'set-s3-public-block',
1751 state={'type': 'boolean', 'default': True},
1752 BlockPublicAcls={'type': 'boolean'},
1753 IgnorePublicAcls={'type': 'boolean'},
1754 BlockPublicPolicy={'type': 'boolean'},
1755 RestrictPublicBuckets={'type': 'boolean'})
1756
1757 permissions = ('s3:PutAccountPublicAccessBlock', 's3:GetAccountPublicAccessBlock')
1758
1759 def validate(self):
1760 config = self.data.copy()
1761 config.pop('type')
1762 if config.pop('state', None) is False and config:
1763 raise PolicyValidationError(
1764 "{} cant set state false with controls specified".format(
1765 self.type))
1766
1767 def process(self, resources):
1768 client = local_session(self.manager.session_factory).client('s3control')
1769 if self.data.get('state', True) is False:
1770 for r in resources:
1771 client.delete_public_access_block(AccountId=r['account_id'])
1772 return
1773
1774 keys = (
1775 'BlockPublicPolicy', 'BlockPublicAcls', 'IgnorePublicAcls', 'RestrictPublicBuckets')
1776
1777 for r in resources:
1778 # try to merge with existing configuration if not explicitly set.
1779 base = {}
1780 if S3PublicBlock.annotation_key in r:
1781 base = r[S3PublicBlock.annotation_key]
1782 else:
1783 try:
1784 base = client.get_public_access_block(AccountId=r['account_id']).get(
1785 'PublicAccessBlockConfiguration')
1786 except client.exceptions.NoSuchPublicAccessBlockConfiguration:
1787 base = {}
1788
1789 config = {}
1790 for k in keys:
1791 if k in self.data:
1792 config[k] = self.data[k]
1793 elif k in base:
1794 config[k] = base[k]
1795
1796 client.put_public_access_block(
1797 AccountId=r['account_id'],
1798 PublicAccessBlockConfiguration=config)
1799
1800
1801@filters.register('ami-block-public-access')
1802class AmiBlockPublicAccess(Filter):
1803 """Scans for AWS accounts that have/do not have Block Public Access set for
1804 their AMIs.
1805
1806 :example:
1807
1808 .. code-block:: yaml
1809
1810 policies:
1811 - name: unblocked-ami-block-public-access
1812 resource: aws.account
1813 filters:
1814 - type: ami-block-public-access
1815 value: false
1816
1817 """
1818
1819 annotation_key = 'c7n:ami-block-public-access'
1820 schema = type_schema('ami-block-public-access', value={'type': 'boolean'})
1821 permissions = ('ec2:GetImageBlockPublicAccessState',)
1822
1823 def process(self, resources, event=None):
1824 client = local_session(self.manager.session_factory).client('ec2')
1825 is_blocked = self.data.get('value', False)
1826 results = []
1827
1828 # Get account-wide block public access state
1829 response = client.get_image_block_public_access_state()
1830 account_state = response.get('ImageBlockPublicAccessState', 'unblocked')
1831 account_blocked = account_state == 'block-new-sharing'
1832
1833 for r in resources:
1834 r[self.annotation_key] = account_blocked
1835
1836 # Filter resources based on whether the account state matches the desired value
1837 if account_blocked == is_blocked:
1838 results.append(r)
1839
1840 return results
1841
1842
1843class GlueCatalogEncryptionEnabled(MultiAttrFilter):
1844 """ Filter glue catalog by its glue encryption status and KMS key
1845
1846 :example:
1847
1848 .. code-block:: yaml
1849
1850 policies:
1851 - name: glue-catalog-security-config
1852 resource: aws.glue-catalog
1853 filters:
1854 - type: glue-security-config
1855 SseAwsKmsKeyId: alias/aws/glue
1856
1857 """
1858 retry = staticmethod(QueryResourceManager.retry)
1859
1860 schema = {
1861 'type': 'object',
1862 'additionalProperties': False,
1863 'properties': {
1864 'type': {'enum': ['glue-security-config']},
1865 'CatalogEncryptionMode': {'enum': ['DISABLED', 'SSE-KMS']},
1866 'SseAwsKmsKeyId': {'type': 'string'},
1867 'ReturnConnectionPasswordEncrypted': {'type': 'boolean'},
1868 'AwsKmsKeyId': {'type': 'string'}
1869 }
1870 }
1871
1872 annotation = "c7n:glue-security-config"
1873 permissions = ('glue:GetDataCatalogEncryptionSettings',)
1874
1875 def validate(self):
1876 attrs = set()
1877 for key in self.data:
1878 if key in ['CatalogEncryptionMode',
1879 'ReturnConnectionPasswordEncrypted',
1880 'SseAwsKmsKeyId',
1881 'AwsKmsKeyId']:
1882 attrs.add(key)
1883 self.multi_attrs = attrs
1884 return super(GlueCatalogEncryptionEnabled, self).validate()
1885
1886 def get_target(self, resource):
1887 if self.annotation in resource:
1888 return resource[self.annotation]
1889 client = local_session(self.manager.session_factory).client('glue')
1890 encryption_setting = resource.get('DataCatalogEncryptionSettings')
1891 if self.manager.type != 'glue-catalog':
1892 encryption_setting = client.get_data_catalog_encryption_settings().get(
1893 'DataCatalogEncryptionSettings')
1894 resource[self.annotation] = encryption_setting.get('EncryptionAtRest')
1895 resource[self.annotation].update(encryption_setting.get('ConnectionPasswordEncryption'))
1896 key_attrs = ('SseAwsKmsKeyId', 'AwsKmsKeyId')
1897 for encrypt_attr in key_attrs:
1898 if encrypt_attr not in self.data or not self.data[encrypt_attr].startswith('alias'):
1899 continue
1900 key = resource[self.annotation].get(encrypt_attr)
1901 vfd = {'c7n:AliasName': self.data[encrypt_attr]}
1902 vf = KmsRelatedFilter(vfd, self.manager)
1903 vf.RelatedIdsExpression = 'KmsKeyId'
1904 vf.annotate = False
1905 if not vf.process([{'KmsKeyId': key}]):
1906 return []
1907 resource[self.annotation][encrypt_attr] = self.data[encrypt_attr]
1908 return resource[self.annotation]
1909
1910
1911@filters.register('glue-security-config')
1912class AccountCatalogEncryptionFilter(GlueCatalogEncryptionEnabled):
1913 """Filter aws account by its glue encryption status and KMS key
1914
1915 :example:
1916
1917 .. code-block:: yaml
1918
1919 policies:
1920 - name: glue-security-config
1921 resource: aws.account
1922 filters:
1923 - type: glue-security-config
1924 SseAwsKmsKeyId: alias/aws/glue
1925
1926 """
1927
1928
1929@filters.register('emr-block-public-access')
1930class EMRBlockPublicAccessConfiguration(ValueFilter):
1931 """Check for EMR block public access configuration on an account
1932
1933 :example:
1934
1935 .. code-block:: yaml
1936
1937 policies:
1938 - name: get-emr-block-public-access
1939 resource: account
1940 filters:
1941 - type: emr-block-public-access
1942 """
1943
1944 annotation_key = 'c7n:emr-block-public-access'
1945 annotate = False # no annotation from value filter
1946 schema = type_schema('emr-block-public-access', rinherit=ValueFilter.schema)
1947 schema_alias = False
1948 permissions = ("elasticmapreduce:GetBlockPublicAccessConfiguration",)
1949
1950 def process(self, resources, event=None):
1951 self.augment([r for r in resources if self.annotation_key not in r])
1952 return super().process(resources, event)
1953
1954 def augment(self, resources):
1955 client = local_session(self.manager.session_factory).client(
1956 'emr', region_name=self.manager.config.region)
1957
1958 for r in resources:
1959 r[self.annotation_key] = self.manager.retry(
1960 client.get_block_public_access_configuration)
1961 r[self.annotation_key].pop('ResponseMetadata')
1962
1963 def __call__(self, r):
1964 return super(EMRBlockPublicAccessConfiguration, self).__call__(r[self.annotation_key])
1965
1966
1967@actions.register('set-emr-block-public-access')
1968class PutAccountBlockPublicAccessConfiguration(BaseAction):
1969 """Action to put/update the EMR block public access configuration for your
1970 AWS account in the current region
1971
1972 :example:
1973
1974 .. code-block:: yaml
1975
1976 policies:
1977 - name: set-emr-block-public-access
1978 resource: account
1979 filters:
1980 - type: emr-block-public-access
1981 key: BlockPublicAccessConfiguration.BlockPublicSecurityGroupRules
1982 value: False
1983 actions:
1984 - type: set-emr-block-public-access
1985 config:
1986 BlockPublicSecurityGroupRules: True
1987 PermittedPublicSecurityGroupRuleRanges:
1988 - MinRange: 22
1989 MaxRange: 22
1990 - MinRange: 23
1991 MaxRange: 23
1992
1993 """
1994
1995 schema = type_schema('set-emr-block-public-access',
1996 config={"type": "object",
1997 'properties': {
1998 'BlockPublicSecurityGroupRules': {'type': 'boolean'},
1999 'PermittedPublicSecurityGroupRuleRanges': {
2000 'type': 'array',
2001 'items': {
2002 'type': 'object',
2003 'properties': {
2004 'MinRange': {'type': 'number', "minimum": 0},
2005 'MaxRange': {'type': 'number', "minimum": 0}
2006 },
2007 'required': ['MinRange']
2008 }
2009 }
2010 },
2011 'required': ['BlockPublicSecurityGroupRules']
2012 },
2013 required=('config',))
2014
2015 permissions = ("elasticmapreduce:PutBlockPublicAccessConfiguration",)
2016
2017 def process(self, resources):
2018 client = local_session(self.manager.session_factory).client('emr')
2019 r = resources[0]
2020
2021 base = {}
2022 if EMRBlockPublicAccessConfiguration.annotation_key in r:
2023 base = r[EMRBlockPublicAccessConfiguration.annotation_key]
2024 else:
2025 try:
2026 base = client.get_block_public_access_configuration()
2027 base.pop('ResponseMetadata')
2028 except client.exceptions.NoSuchPublicAccessBlockConfiguration:
2029 base = {}
2030
2031 config = base['BlockPublicAccessConfiguration']
2032 updatedConfig = {**config, **self.data.get('config')}
2033
2034 if config == updatedConfig:
2035 return
2036
2037 client.put_block_public_access_configuration(
2038 BlockPublicAccessConfiguration=updatedConfig
2039 )
2040
2041
2042@filters.register('securityhub')
2043class SecHubEnabled(Filter):
2044 """Filter an account depending on whether security hub is enabled or not.
2045
2046 :example:
2047
2048 .. code-block:: yaml
2049
2050 policies:
2051 - name: check-securityhub-status
2052 resource: aws.account
2053 filters:
2054 - type: securityhub
2055 enabled: true
2056
2057 """
2058
2059 permissions = ('securityhub:DescribeHub',)
2060
2061 schema = type_schema('securityhub', enabled={'type': 'boolean'})
2062
2063 def process(self, resources, event=None):
2064 state = self.data.get('enabled', True)
2065 client = local_session(self.manager.session_factory).client('securityhub')
2066 sechub = self.manager.retry(client.describe_hub, ignore_err_codes=(
2067 'InvalidAccessException',))
2068 if state == bool(sechub):
2069 return resources
2070 return []
2071
2072
2073@filters.register('lakeformation-s3-cross-account')
2074class LakeformationFilter(Filter):
2075 """Flags an account if its using a lakeformation s3 bucket resource from a different account.
2076
2077 :example:
2078
2079 .. code-block:: yaml
2080
2081 policies:
2082 - name: lakeformation-cross-account-bucket
2083 resource: aws.account
2084 filters:
2085 - type: lakeformation-s3-cross-account
2086
2087 """
2088
2089 schema = type_schema('lakeformation-s3-cross-account', rinherit=ValueFilter.schema)
2090 schema_alias = False
2091 permissions = ('lakeformation:ListResources',)
2092 annotation = 'c7n:lake-cross-account-s3'
2093
2094 def process(self, resources, event=None):
2095 results = []
2096 for r in resources:
2097 if self.process_account(r):
2098 results.append(r)
2099 return results
2100
2101 def process_account(self, account):
2102 client = local_session(self.manager.session_factory).client('lakeformation')
2103 lake_buckets = {
2104 Arn.parse(r).resource for r in jmespath_search(
2105 'ResourceInfoList[].ResourceArn',
2106 client.list_resources())
2107 }
2108 buckets = {
2109 b['Name'] for b in
2110 self.manager.get_resource_manager('s3').resources(augment=False)}
2111 cross_account = lake_buckets.difference(buckets)
2112 if not cross_account:
2113 return False
2114 account[self.annotation] = list(cross_account)
2115 return True
2116
2117
2118@actions.register('toggle-config-managed-rule')
2119class ToggleConfigManagedRule(BaseAction):
2120 """Enables or disables an AWS Config Managed Rule
2121
2122 :example:
2123
2124 .. code-block:: yaml
2125
2126 policies:
2127 - name: config-managed-s3-bucket-public-write-remediate-event
2128 description: |
2129 This policy detects if S3 bucket allows public write by the bucket policy
2130 or ACL and remediates.
2131 comment: |
2132 This policy detects if S3 bucket policy or ACL allows public write access.
2133 When the bucket is evaluated as 'NON_COMPLIANT', the action
2134 'AWS-DisableS3BucketPublicReadWrite' is triggered and remediates.
2135 resource: account
2136 filters:
2137 - type: missing
2138 policy:
2139 resource: config-rule
2140 filters:
2141 - type: remediation
2142 rule_name: &rule_name 'config-managed-s3-bucket-public-write-remediate-event'
2143 remediation: &remediation-config
2144 TargetId: AWS-DisableS3BucketPublicReadWrite
2145 Automatic: true
2146 MaximumAutomaticAttempts: 5
2147 RetryAttemptSeconds: 211
2148 Parameters:
2149 AutomationAssumeRole:
2150 StaticValue:
2151 Values:
2152 - 'arn:aws:iam::{account_id}:role/myrole'
2153 S3BucketName:
2154 ResourceValue:
2155 Value: RESOURCE_ID
2156 actions:
2157 - type: toggle-config-managed-rule
2158 rule_name: *rule_name
2159 managed_rule_id: S3_BUCKET_PUBLIC_WRITE_PROHIBITED
2160 resource_types:
2161 - 'AWS::S3::Bucket'
2162 rule_parameters: '{}'
2163 remediation: *remediation-config
2164 """
2165
2166 permissions = (
2167 'config:DescribeConfigRules',
2168 'config:DescribeRemediationConfigurations',
2169 'config:PutRemediationConfigurations',
2170 'config:PutConfigRule',
2171 )
2172
2173 schema = type_schema('toggle-config-managed-rule',
2174 enabled={'type': 'boolean', 'default': True},
2175 rule_name={'type': 'string'},
2176 rule_prefix={'type': 'string'},
2177 managed_rule_id={'type': 'string'},
2178 resource_types={'type': 'array', 'items':
2179 {'pattern': '^AWS::*', 'type': 'string'}},
2180 resource_tag={
2181 'type': 'object',
2182 'properties': {
2183 'key': {'type': 'string'},
2184 'value': {'type': 'string'},
2185 },
2186 'required': ['key', 'value'],
2187 },
2188 resource_id={'type': 'string'},
2189 rule_parameters={'type': 'string'},
2190 remediation={
2191 'type': 'object',
2192 'properties': {
2193 'TargetType': {'type': 'string'},
2194 'TargetId': {'type': 'string'},
2195 'Automatic': {'type': 'boolean'},
2196 'Parameters': {'type': 'object'},
2197 'MaximumAutomaticAttempts': {
2198 'type': 'integer',
2199 'minimum': 1, 'maximum': 25,
2200 },
2201 'RetryAttemptSeconds': {
2202 'type': 'integer',
2203 'minimum': 1, 'maximum': 2678000,
2204 },
2205 'ExecutionControls': {'type': 'object'},
2206 },
2207 },
2208 tags={'type': 'object'},
2209 required=['rule_name'],
2210 )
2211
2212 def validate(self):
2213 if (
2214 self.data.get('enabled', True) and
2215 not self.data.get('managed_rule_id')
2216 ):
2217 raise PolicyValidationError("managed_rule_id required to enable a managed rule")
2218 return self
2219
2220 def process(self, accounts):
2221 client = local_session(self.manager.session_factory).client('config')
2222 rule = self.ConfigManagedRule(self.data)
2223 params = self.get_rule_params(rule)
2224
2225 if self.data.get('enabled', True):
2226 client.put_config_rule(**params)
2227
2228 if rule.remediation:
2229 remediation_params = self.get_remediation_params(rule)
2230 client.put_remediation_configurations(
2231 RemediationConfigurations=[remediation_params]
2232 )
2233 else:
2234 with suppress(client.exceptions.NoSuchRemediationConfigurationException):
2235 client.delete_remediation_configuration(
2236 ConfigRuleName=rule.name
2237 )
2238
2239 with suppress(client.exceptions.NoSuchConfigRuleException):
2240 client.delete_config_rule(
2241 ConfigRuleName=rule.name
2242 )
2243
2244 def get_rule_params(self, rule):
2245 params = dict(
2246 ConfigRuleName=rule.name,
2247 Description=rule.description,
2248 Source={
2249 'Owner': 'AWS',
2250 'SourceIdentifier': rule.managed_rule_id,
2251 },
2252 InputParameters=rule.rule_parameters
2253 )
2254
2255 # A config rule scope can include one or more resource types,
2256 # a combination of a tag key and value, or a combination of
2257 # one resource type and one resource ID
2258 params.update({'Scope': {'ComplianceResourceTypes': rule.resource_types}})
2259 if rule.resource_tag:
2260 params.update({'Scope': {
2261 'TagKey': rule.resource_tag['key'],
2262 'TagValue': rule.resource_tag['value']}
2263 })
2264 elif rule.resource_id:
2265 params.update({'Scope': {'ComplianceResourceId': rule.resource_id}})
2266
2267 return dict(ConfigRule=params)
2268
2269 def get_remediation_params(self, rule):
2270 rule.remediation['ConfigRuleName'] = rule.name
2271 if 'TargetType' not in rule.remediation:
2272 rule.remediation['TargetType'] = 'SSM_DOCUMENT'
2273 return rule.remediation
2274
2275 class ConfigManagedRule:
2276 """Wraps the action data into an AWS Config Managed Rule.
2277 """
2278
2279 def __init__(self, data):
2280 self.data = data
2281
2282 @property
2283 def name(self):
2284 prefix = self.data.get('rule_prefix', 'custodian-')
2285 return "%s%s" % (prefix, self.data.get('rule_name', ''))
2286
2287 @property
2288 def description(self):
2289 return self.data.get(
2290 'description', 'cloud-custodian AWS Config Managed Rule policy')
2291
2292 @property
2293 def tags(self):
2294 return self.data.get('tags', {})
2295
2296 @property
2297 def resource_types(self):
2298 return self.data.get('resource_types', [])
2299
2300 @property
2301 def managed_rule_id(self):
2302 return self.data.get('managed_rule_id', '')
2303
2304 @property
2305 def resource_tag(self):
2306 return self.data.get('resource_tag', {})
2307
2308 @property
2309 def resource_id(self):
2310 return self.data.get('resource_id', '')
2311
2312 @property
2313 def rule_parameters(self):
2314 return self.data.get('rule_parameters', '')
2315
2316 @property
2317 def remediation(self):
2318 return self.data.get('remediation', {})
2319
2320
2321@filters.register('ses-agg-send-stats')
2322class SesAggStats(ValueFilter):
2323 """This filter queries SES send statistics and aggregates all
2324 the data points into a single report.
2325
2326 :example:
2327
2328 .. code-block:: yaml
2329
2330 policies:
2331 - name: ses-aggregated-send-stats-policy
2332 resource: account
2333 filters:
2334 - type: ses-agg-send-stats
2335 """
2336
2337 schema = type_schema('ses-agg-send-stats', rinherit=ValueFilter.schema)
2338 annotation_key = 'c7n:ses-send-agg'
2339 permissions = ("ses:GetSendStatistics",)
2340
2341 def process(self, resources, event=None):
2342 client = local_session(self.manager.session_factory).client('ses')
2343 get_send_stats = client.get_send_statistics()
2344 results = []
2345
2346 if not get_send_stats or not get_send_stats.get('SendDataPoints'):
2347 return results
2348
2349 resource_counter = {'DeliveryAttempts': 0,
2350 'Bounces': 0,
2351 'Complaints': 0,
2352 'Rejects': 0,
2353 'BounceRate': 0}
2354 for d in get_send_stats.get('SendDataPoints', []):
2355 resource_counter['DeliveryAttempts'] += d['DeliveryAttempts']
2356 resource_counter['Bounces'] += d['Bounces']
2357 resource_counter['Complaints'] += d['Complaints']
2358 resource_counter['Rejects'] += d['Rejects']
2359 resource_counter['BounceRate'] = round(
2360 (resource_counter['Bounces'] /
2361 resource_counter['DeliveryAttempts']) * 100)
2362 resources[0][self.annotation_key] = resource_counter
2363
2364 return resources
2365
2366
2367@filters.register('ses-send-stats')
2368class SesConsecutiveStats(Filter):
2369 """This filter annotates the account resource with SES send statistics for the
2370 last n number of days, not including the current date.
2371
2372 The stats are aggregated into daily metrics. Additionally, the filter also
2373 calculates and annotates the max daily bounce rate (percentage). Using this filter,
2374 users can alert when the bounce rate for a particular day is higher than the limit.
2375
2376 :example:
2377
2378 .. code-block:: yaml
2379
2380 policies:
2381 - name: ses-send-stats
2382 resource: account
2383 filters:
2384 - type: ses-send-stats
2385 days: 5
2386 - type: value
2387 key: '"c7n:ses-max-bounce-rate"'
2388 op: ge
2389 value: 10
2390 """
2391 schema = type_schema('ses-send-stats', days={'type': 'number', 'minimum': 2},
2392 required=['days'])
2393 send_stats_annotation = 'c7n:ses-send-stats'
2394 max_bounce_annotation = 'c7n:ses-max-bounce-rate'
2395 permissions = ("ses:GetSendStatistics",)
2396
2397 def process(self, resources, event=None):
2398 client = local_session(self.manager.session_factory).client('ses')
2399 get_send_stats = client.get_send_statistics()
2400 results = []
2401 check_days = self.data.get('days', 2)
2402 utcnow = datetime.datetime.utcnow()
2403 expected_dates = set()
2404
2405 for days in range(1, check_days + 1):
2406 expected_dates.add((utcnow - datetime.timedelta(days=days)).strftime('%Y-%m-%d'))
2407
2408 if not get_send_stats or not get_send_stats.get('SendDataPoints'):
2409 return results
2410
2411 metrics = {}
2412 for d in get_send_stats.get('SendDataPoints', []):
2413 ts = d['Timestamp'].strftime('%Y-%m-%d')
2414 if ts not in expected_dates:
2415 continue
2416
2417 if not metrics.get(ts):
2418 metrics[ts] = {'DeliveryAttempts': 0,
2419 'Bounces': 0,
2420 'Complaints': 0,
2421 'Rejects': 0}
2422 metrics[ts]['DeliveryAttempts'] += d['DeliveryAttempts']
2423 metrics[ts]['Bounces'] += d['Bounces']
2424 metrics[ts]['Complaints'] += d['Complaints']
2425 metrics[ts]['Rejects'] += d['Rejects']
2426
2427 max_bounce_rate = 0
2428 for ts, metric in metrics.items():
2429 metric['BounceRate'] = round((metric['Bounces'] / metric['DeliveryAttempts']) * 100)
2430 if max_bounce_rate < metric['BounceRate']:
2431 max_bounce_rate = metric['BounceRate']
2432 metric['Date'] = ts
2433
2434 resources[0][self.send_stats_annotation] = list(metrics.values())
2435 resources[0][self.max_bounce_annotation] = max_bounce_rate
2436
2437 return resources
2438
2439
2440@filters.register('bedrock-model-invocation-logging')
2441class BedrockModelInvocationLogging(ListItemFilter):
2442 """Filter for account to look at bedrock model invocation logging configuration
2443
2444 The schema to supply to the attrs follows the schema here:
2445 https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock/client/get_model_invocation_logging_configuration.html
2446
2447 :example:
2448
2449 .. code-block:: yaml
2450
2451 policies:
2452 - name: bedrock-model-invocation-logging-configuration
2453 resource: account
2454 filters:
2455 - type: bedrock-model-invocation-logging
2456 attrs:
2457 - imageDataDeliveryEnabled: True
2458
2459 """
2460 schema = type_schema(
2461 'bedrock-model-invocation-logging',
2462 attrs={'$ref': '#/definitions/filters_common/list_item_attrs'},
2463 count={'type': 'number'},
2464 count_op={'$ref': '#/definitions/filters_common/comparison_operators'}
2465 )
2466 permissions = ('bedrock:GetModelInvocationLoggingConfiguration',)
2467 annotation_key = 'c7n:BedrockModelInvocationLogging'
2468
2469 def get_item_values(self, resource):
2470 item_values = []
2471 client = local_session(self.manager.session_factory).client('bedrock')
2472 invocation_logging_config = client \
2473 .get_model_invocation_logging_configuration().get('loggingConfig')
2474 if invocation_logging_config is not None:
2475 item_values.append(invocation_logging_config)
2476 resource[self.annotation_key] = invocation_logging_config
2477 return item_values
2478
2479
2480@actions.register('set-bedrock-model-invocation-logging')
2481class SetBedrockModelInvocationLogging(BaseAction):
2482 """Set Bedrock Model Invocation Logging Configuration on an account.
2483 https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock/client/put_model_invocation_logging_configuration.html
2484
2485 To delete a configuration, supply enabled to False
2486
2487 :example:
2488
2489 .. code-block:: yaml
2490
2491 policies:
2492 - name: set-bedrock-model-invocation-logging
2493 resource: account
2494 actions:
2495 - type: set-bedrock-model-invocation-logging
2496 enabled: True
2497 loggingConfig:
2498 textDataDeliveryEnabled: True
2499 s3Config:
2500 bucketName: test-bedrock-1
2501 keyPrefix: logging/
2502
2503 - name: delete-bedrock-model-invocation-logging
2504 resource: account
2505 actions:
2506 - type: set-bedrock-model-invocation-logging
2507 enabled: False
2508 """
2509
2510 schema = {
2511 'type': 'object',
2512 'additionalProperties': False,
2513 'properties': {
2514 'type': {'enum': ['set-bedrock-model-invocation-logging']},
2515 'enabled': {'type': 'boolean'},
2516 'loggingConfig': {'type': 'object'}
2517 },
2518 }
2519
2520 permissions = ('bedrock:PutModelInvocationLoggingConfiguration',)
2521 shape = 'PutModelInvocationLoggingConfigurationRequest'
2522 service = 'bedrock'
2523
2524 def validate(self):
2525 cfg = dict(self.data)
2526 enabled = cfg.get('enabled')
2527 if enabled:
2528 cfg.pop('type')
2529 cfg.pop('enabled')
2530 return shape_validate(
2531 cfg,
2532 self.shape,
2533 self.service)
2534
2535 def process(self, resources):
2536 client = local_session(self.manager.session_factory).client('bedrock')
2537 if self.data.get('enabled'):
2538 params = self.data.get('loggingConfig')
2539 client.put_model_invocation_logging_configuration(loggingConfig=params)
2540 else:
2541 client.delete_model_invocation_logging_configuration()
2542
2543
2544@filters.register('ec2-metadata-defaults')
2545class EC2MetadataDefaults(ValueFilter):
2546 """Filter on the default instance metadata service (IMDS) settings for the specified account and
2547 region. NOTE: Any configuration that has never been set (or is set to 'No Preference'), will
2548 not be returned in the response.
2549
2550 :example:
2551
2552 .. code-block:: yaml
2553
2554 policies:
2555 - name: ec2-imds-defaults
2556 resource: account
2557 filters:
2558 - or:
2559 - type: ec2-metadata-defaults
2560 key: HttpTokens
2561 value: optional
2562 - type: ec2-metadata-defaults
2563 key: HttpTokens
2564 value: absent
2565 """
2566
2567 annotation_key = 'c7n:EC2MetadataDefaults'
2568 annotate = False # no annotation from value filter
2569 schema = type_schema('ec2-metadata-defaults', rinherit=ValueFilter.schema)
2570 permissions = ('ec2:GetInstanceMetadataDefaults',)
2571
2572 def augment(self, resources):
2573 client = local_session(self.manager.session_factory).client('ec2')
2574 for r in resources:
2575 r[self.annotation_key] = self.manager.retry(
2576 client.get_instance_metadata_defaults)["AccountLevel"]
2577
2578 def process(self, resources, event=None):
2579 self.augment([r for r in resources if self.annotation_key not in r])
2580 return super(EC2MetadataDefaults, self).process(resources, event)
2581
2582 def __call__(self, r):
2583 return super(EC2MetadataDefaults, self).__call__(r[self.annotation_key])
2584
2585
2586@actions.register('set-ec2-metadata-defaults')
2587class SetEC2MetadataDefaults(BaseAction):
2588 """Modifies the default instance metadata service (IMDS) settings at the account level.
2589
2590 :example:
2591
2592 .. code-block:: yaml
2593
2594 policies:
2595 - name: set-ec2-metadata-defaults
2596 resource: account
2597 filters:
2598 - or:
2599 - type: ec2-metadata-defaults
2600 key: HttpTokens
2601 op: eq
2602 value: optional
2603 - type: ec2-metadata-defaults
2604 key: HttpTokens
2605 value: absent
2606 actions:
2607 - type: set-ec2-metadata-defaults
2608 HttpTokens: required
2609
2610 """
2611
2612 schema = type_schema(
2613 'set-ec2-metadata-defaults',
2614 HttpTokens={'enum': ['optional', 'required', 'no-preference']},
2615 HttpPutResponseHopLimit={'type': 'integer'},
2616 HttpEndpoint={'enum': ['enabled', 'disabled', 'no-preference']},
2617 InstanceMetadataTags={'enum': ['enabled', 'disabled', 'no-preference']},
2618 )
2619
2620 permissions = ('ec2:ModifyInstanceMetadataDefaults',)
2621 service = 'ec2'
2622 shape = 'ModifyInstanceMetadataDefaultsRequest'
2623
2624 def validate(self):
2625 req = dict(self.data)
2626 req.pop('type')
2627 return shape_validate(
2628 req, self.shape, self.service
2629 )
2630
2631 def process(self, resources):
2632 client = local_session(self.manager.session_factory).client(self.service)
2633 self.data.pop('type')
2634 client.modify_instance_metadata_defaults(**self.data)
2635
2636
2637@actions.register('set-security-token-service-preferences')
2638class SetSecurityTokenServicePreferences(BaseAction):
2639 """Action to set STS preferences."""
2640
2641 """Action to set STS preferences.
2642
2643 :example:
2644
2645 .. code-block:: yaml
2646
2647 policies:
2648 - name: set-sts-preferences
2649 resource: account
2650 filters:
2651 - or:
2652 - type: iam-summary
2653 key: GlobalEndpointTokenVersion
2654 value: absent
2655 value: optional
2656 - type: iam-summary
2657 key: GlobalEndpointTokenVersion
2658 op: ne
2659 value: 2
2660 actions:
2661 - type: set-security-token-service-preferences
2662 token_version: v2Token
2663
2664 """
2665
2666 schema = type_schema(
2667 'set-security-token-service-preferences',
2668 token_version={'type': 'string', 'enum': ['v1Token', 'v2Token']}
2669 )
2670
2671 permissions = ('iam:SetSecurityTokenServicePreferences',)
2672
2673 def process(self, resources):
2674 client = local_session(self.manager.session_factory).client('iam')
2675 token_version = self.data.get('token_version', 'v2Token')
2676 for resource in resources:
2677 self.set_sts_preferences(client, token_version)
2678
2679 def set_sts_preferences(self, client, token_version):
2680 client.set_security_token_service_preferences(
2681 GlobalEndpointTokenVersion=token_version
2682 )