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 Master.AccountId: "00011001"
411 Master.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 '^Master': {'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 master = client.get_administrator_account(DetectorId=detector_id).get('Master')
455 resource[self.annotation] = r = {'Detector': detector, 'Master': master}
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
1801class GlueCatalogEncryptionEnabled(MultiAttrFilter):
1802 """ Filter glue catalog by its glue encryption status and KMS key
1803
1804 :example:
1805
1806 .. code-block:: yaml
1807
1808 policies:
1809 - name: glue-catalog-security-config
1810 resource: aws.glue-catalog
1811 filters:
1812 - type: glue-security-config
1813 SseAwsKmsKeyId: alias/aws/glue
1814
1815 """
1816 retry = staticmethod(QueryResourceManager.retry)
1817
1818 schema = {
1819 'type': 'object',
1820 'additionalProperties': False,
1821 'properties': {
1822 'type': {'enum': ['glue-security-config']},
1823 'CatalogEncryptionMode': {'enum': ['DISABLED', 'SSE-KMS']},
1824 'SseAwsKmsKeyId': {'type': 'string'},
1825 'ReturnConnectionPasswordEncrypted': {'type': 'boolean'},
1826 'AwsKmsKeyId': {'type': 'string'}
1827 }
1828 }
1829
1830 annotation = "c7n:glue-security-config"
1831 permissions = ('glue:GetDataCatalogEncryptionSettings',)
1832
1833 def validate(self):
1834 attrs = set()
1835 for key in self.data:
1836 if key in ['CatalogEncryptionMode',
1837 'ReturnConnectionPasswordEncrypted',
1838 'SseAwsKmsKeyId',
1839 'AwsKmsKeyId']:
1840 attrs.add(key)
1841 self.multi_attrs = attrs
1842 return super(GlueCatalogEncryptionEnabled, self).validate()
1843
1844 def get_target(self, resource):
1845 if self.annotation in resource:
1846 return resource[self.annotation]
1847 client = local_session(self.manager.session_factory).client('glue')
1848 encryption_setting = resource.get('DataCatalogEncryptionSettings')
1849 if self.manager.type != 'glue-catalog':
1850 encryption_setting = client.get_data_catalog_encryption_settings().get(
1851 'DataCatalogEncryptionSettings')
1852 resource[self.annotation] = encryption_setting.get('EncryptionAtRest')
1853 resource[self.annotation].update(encryption_setting.get('ConnectionPasswordEncryption'))
1854 key_attrs = ('SseAwsKmsKeyId', 'AwsKmsKeyId')
1855 for encrypt_attr in key_attrs:
1856 if encrypt_attr not in self.data or not self.data[encrypt_attr].startswith('alias'):
1857 continue
1858 key = resource[self.annotation].get(encrypt_attr)
1859 vfd = {'c7n:AliasName': self.data[encrypt_attr]}
1860 vf = KmsRelatedFilter(vfd, self.manager)
1861 vf.RelatedIdsExpression = 'KmsKeyId'
1862 vf.annotate = False
1863 if not vf.process([{'KmsKeyId': key}]):
1864 return []
1865 resource[self.annotation][encrypt_attr] = self.data[encrypt_attr]
1866 return resource[self.annotation]
1867
1868
1869@filters.register('glue-security-config')
1870class AccountCatalogEncryptionFilter(GlueCatalogEncryptionEnabled):
1871 """Filter aws account by its glue encryption status and KMS key
1872
1873 :example:
1874
1875 .. code-block:: yaml
1876
1877 policies:
1878 - name: glue-security-config
1879 resource: aws.account
1880 filters:
1881 - type: glue-security-config
1882 SseAwsKmsKeyId: alias/aws/glue
1883
1884 """
1885
1886
1887@filters.register('emr-block-public-access')
1888class EMRBlockPublicAccessConfiguration(ValueFilter):
1889 """Check for EMR block public access configuration on an account
1890
1891 :example:
1892
1893 .. code-block:: yaml
1894
1895 policies:
1896 - name: get-emr-block-public-access
1897 resource: account
1898 filters:
1899 - type: emr-block-public-access
1900 """
1901
1902 annotation_key = 'c7n:emr-block-public-access'
1903 annotate = False # no annotation from value filter
1904 schema = type_schema('emr-block-public-access', rinherit=ValueFilter.schema)
1905 schema_alias = False
1906 permissions = ("elasticmapreduce:GetBlockPublicAccessConfiguration",)
1907
1908 def process(self, resources, event=None):
1909 self.augment([r for r in resources if self.annotation_key not in r])
1910 return super().process(resources, event)
1911
1912 def augment(self, resources):
1913 client = local_session(self.manager.session_factory).client(
1914 'emr', region_name=self.manager.config.region)
1915
1916 for r in resources:
1917 r[self.annotation_key] = self.manager.retry(
1918 client.get_block_public_access_configuration)
1919 r[self.annotation_key].pop('ResponseMetadata')
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()
2500
2501
2502@filters.register('ec2-metadata-defaults')
2503class EC2MetadataDefaults(ValueFilter):
2504 """Filter on the default instance metadata service (IMDS) settings for the specified account and
2505 region. NOTE: Any configuration that has never been set (or is set to 'No Preference'), will
2506 not be returned in the response.
2507
2508 :example:
2509
2510 .. code-block:: yaml
2511
2512 policies:
2513 - name: ec2-imds-defaults
2514 resource: account
2515 filters:
2516 - or:
2517 - type: ec2-metadata-defaults
2518 key: HttpTokens
2519 value: optional
2520 - type: ec2-metadata-defaults
2521 key: HttpTokens
2522 value: absent
2523 """
2524
2525 annotation_key = 'c7n:EC2MetadataDefaults'
2526 annotate = False # no annotation from value filter
2527 schema = type_schema('ec2-metadata-defaults', rinherit=ValueFilter.schema)
2528 permissions = ('ec2:GetInstanceMetadataDefaults',)
2529
2530 def augment(self, resources):
2531 client = local_session(self.manager.session_factory).client('ec2')
2532 for r in resources:
2533 r[self.annotation_key] = self.manager.retry(
2534 client.get_instance_metadata_defaults)["AccountLevel"]
2535
2536 def process(self, resources, event=None):
2537 self.augment([r for r in resources if self.annotation_key not in r])
2538 return super(EC2MetadataDefaults, self).process(resources, event)
2539
2540 def __call__(self, r):
2541 return super(EC2MetadataDefaults, self).__call__(r[self.annotation_key])
2542
2543
2544@actions.register('set-ec2-metadata-defaults')
2545class SetEC2MetadataDefaults(BaseAction):
2546 """Modifies the default instance metadata service (IMDS) settings at the account level.
2547
2548 :example:
2549
2550 .. code-block:: yaml
2551
2552 policies:
2553 - name: set-ec2-metadata-defaults
2554 resource: account
2555 filters:
2556 - or:
2557 - type: ec2-metadata-defaults
2558 key: HttpTokens
2559 op: eq
2560 value: optional
2561 - type: ec2-metadata-defaults
2562 key: HttpTokens
2563 value: absent
2564 actions:
2565 - type: set-ec2-metadata-defaults
2566 HttpTokens: required
2567
2568 """
2569
2570 schema = type_schema(
2571 'set-ec2-metadata-defaults',
2572 HttpTokens={'enum': ['optional', 'required', 'no-preference']},
2573 HttpPutResponseHopLimit={'type': 'integer'},
2574 HttpEndpoint={'enum': ['enabled', 'disabled', 'no-preference']},
2575 InstanceMetadataTags={'enum': ['enabled', 'disabled', 'no-preference']},
2576 )
2577
2578 permissions = ('ec2:ModifyInstanceMetadataDefaults',)
2579 service = 'ec2'
2580 shape = 'ModifyInstanceMetadataDefaultsRequest'
2581
2582 def validate(self):
2583 req = dict(self.data)
2584 req.pop('type')
2585 return shape_validate(
2586 req, self.shape, self.service
2587 )
2588
2589 def process(self, resources):
2590 client = local_session(self.manager.session_factory).client(self.service)
2591 self.data.pop('type')
2592 client.modify_instance_metadata_defaults(**self.data)
2593
2594
2595@actions.register('set-security-token-service-preferences')
2596class SetSecurityTokenServicePreferences(BaseAction):
2597 """Action to set STS preferences."""
2598
2599 """Action to set STS preferences.
2600
2601 :example:
2602
2603 .. code-block:: yaml
2604
2605 policies:
2606 - name: set-sts-preferences
2607 resource: account
2608 filters:
2609 - or:
2610 - type: iam-summary
2611 key: GlobalEndpointTokenVersion
2612 value: absent
2613 value: optional
2614 - type: iam-summary
2615 key: GlobalEndpointTokenVersion
2616 op: ne
2617 value: 2
2618 actions:
2619 - type: set-security-token-service-preferences
2620 token_version: v2Token
2621
2622 """
2623
2624 schema = type_schema(
2625 'set-security-token-service-preferences',
2626 token_version={'type': 'string', 'enum': ['v1Token', 'v2Token']}
2627 )
2628
2629 permissions = ('iam:SetSecurityTokenServicePreferences',)
2630
2631 def process(self, resources):
2632 client = local_session(self.manager.session_factory).client('iam')
2633 token_version = self.data.get('token_version', 'v2Token')
2634 for resource in resources:
2635 self.set_sts_preferences(client, token_version)
2636
2637 def set_sts_preferences(self, client, token_version):
2638 client.set_security_token_service_preferences(
2639 GlobalEndpointTokenVersion=token_version
2640 )