# Copyright 2016-2017 Capital One Services, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""AWS Account as a custodian resource.
"""
from __future__ import absolute_import, division, print_function, unicode_literals
import json
from botocore.exceptions import ClientError
from datetime import datetime, timedelta
from dateutil.parser import parse as parse_date
from dateutil.tz import tzutc
from c7n.actions import ActionRegistry, BaseAction
from c7n.actions.securityhub import OtherResourcePostFinding
from c7n.exceptions import PolicyValidationError
from c7n.filters import Filter, FilterRegistry, ValueFilter
from c7n.filters.multiattr import MultiAttrFilter
from c7n.filters.missing import Missing
from c7n.manager import ResourceManager, resources
from c7n.utils import local_session, type_schema, generate_arn
from c7n.resources.iam import CredentialReport
filters = FilterRegistry('aws.account.actions')
actions = ActionRegistry('aws.account.filters')
filters.register('missing', Missing)
[docs]def get_account(session_factory, config):
session = local_session(session_factory)
client = session.client('iam')
aliases = client.list_account_aliases().get(
'AccountAliases', ('',))
name = aliases and aliases[0] or ""
return {'account_id': config.account_id,
'account_name': name}
[docs]@resources.register('account')
class Account(ResourceManager):
filter_registry = filters
action_registry = actions
[docs] class resource_type(object):
id = 'account_id'
name = 'account_name'
filter_name = None
[docs] @classmethod
def get_permissions(cls):
return ('iam:ListAccountAliases',)
[docs] @classmethod
def has_arn(cls):
return True
[docs] def get_arns(self, resources):
return ["arn:::{account_id}".format(**r) for r in resources]
[docs] def get_model(self):
return self.resource_type
[docs] def resources(self):
return self.filter_resources([get_account(self.session_factory, self.config)])
[docs] def get_resources(self, resource_ids):
return [get_account(self.session_factory, self.config)]
[docs]@filters.register('credential')
class AccountCredentialReport(CredentialReport):
[docs] def process(self, resources, event=None):
super(AccountCredentialReport, self).process(resources, event)
report = self.get_credential_report()
if report is None:
return []
results = []
info = report.get('<root_account>')
for r in resources:
if self.match(r, info):
r['c7n:credential-report'] = info
results.append(r)
return results
[docs]@filters.register('check-cloudtrail')
class CloudTrailEnabled(Filter):
"""Verify cloud trail enabled for this account per specifications.
Returns an annotated account resource if trail is not enabled.
Of particular note, the current-region option will evaluate whether cloudtrail is available
in the current region, either as a multi region trail or as a trail with it as the home region.
:example:
.. code-block:: yaml
policies:
- name: account-cloudtrail-enabled
resource: account
region: us-east-1
filters:
- type: check-cloudtrail
global-events: true
multi-region: true
running: true
"""
schema = type_schema(
'check-cloudtrail',
**{'multi-region': {'type': 'boolean'},
'global-events': {'type': 'boolean'},
'current-region': {'type': 'boolean'},
'running': {'type': 'boolean'},
'notifies': {'type': 'boolean'},
'file-digest': {'type': 'boolean'},
'kms': {'type': 'boolean'},
'kms-key': {'type': 'string'}})
permissions = ('cloudtrail:DescribeTrails', 'cloudtrail:GetTrailStatus')
[docs] def process(self, resources, event=None):
session = local_session(self.manager.session_factory)
client = session.client('cloudtrail')
trails = client.describe_trails()['trailList']
resources[0]['c7n:cloudtrails'] = trails
if self.data.get('global-events'):
trails = [t for t in trails if t.get('IncludeGlobalServiceEvents')]
if self.data.get('current-region'):
current_region = session.region_name
trails = [t for t in trails if t.get(
'HomeRegion') == current_region or t.get('IsMultiRegionTrail')]
if self.data.get('kms'):
trails = [t for t in trails if t.get('KmsKeyId')]
if self.data.get('kms-key'):
trails = [t for t in trails
if t.get('KmsKeyId', '') == self.data['kms-key']]
if self.data.get('file-digest'):
trails = [t for t in trails
if t.get('LogFileValidationEnabled')]
if self.data.get('multi-region'):
trails = [t for t in trails if t.get('IsMultiRegionTrail')]
if self.data.get('notifies'):
trails = [t for t in trails if t.get('SnsTopicARN')]
if self.data.get('running', True):
running = []
for t in list(trails):
t['Status'] = status = client.get_trail_status(
Name=t['TrailARN'])
if status['IsLogging'] and not status.get(
'LatestDeliveryError'):
running.append(t)
trails = running
if trails:
return []
return resources
[docs]@filters.register('guard-duty')
class GuardDutyEnabled(MultiAttrFilter):
"""Check if the guard duty service is enabled.
This allows looking at account's detector and its associated
master if any.
:example:
Check to ensure guard duty is active on account and associated to a master.
.. code-block:: yaml
policies:
- name: guardduty-enabled
resource: account
filters:
- type: guard-duty
Detector.Status: ENABLED
Master.AccountId: "00011001"
Master.RelationshipStatus: ENABLED
"""
schema = {
'type': 'object',
'additionalProperties': False,
'properties': {
'type': {'enum': ['guard-duty']},
'match-operator': {'enum': ['or', 'and']}},
'patternProperties': {
'^Detector': {'oneOf': [{'type': 'object'}, {'type': 'string'}]},
'^Master': {'oneOf': [{'type': 'object'}, {'type': 'string'}]}},
}
annotation = "c7n:guard-duty"
permissions = (
'guardduty:GetMasterAccount',
'guardduty:ListDetectors',
'guardduty:GetDetector')
[docs] def validate(self):
attrs = set()
for k in self.data:
if k.startswith('Detector') or k.startswith('Master'):
attrs.add(k)
self.multi_attrs = attrs
return super(GuardDutyEnabled, self).validate()
[docs] def get_target(self, resource):
if self.annotation in resource:
return resource[self.annotation]
client = local_session(self.manager.session_factory).client('guardduty')
# detectors are singletons too.
detector_ids = client.list_detectors().get('DetectorIds')
if not detector_ids:
return None
else:
detector_id = detector_ids.pop()
detector = client.get_detector(DetectorId=detector_id)
detector.pop('ResponseMetadata', None)
master = client.get_master_account(DetectorId=detector_id).get('Master')
resource[self.annotation] = r = {'Detector': detector, 'Master': master}
return r
[docs]@filters.register('check-config')
class ConfigEnabled(Filter):
"""Is config service enabled for this account
:example:
.. code-block:: yaml
policies:
- name: account-check-config-services
resource: account
region: us-east-1
filters:
- type: check-config
all-resources: true
global-resources: true
running: true
"""
schema = type_schema(
'check-config', **{
'all-resources': {'type': 'boolean'},
'running': {'type': 'boolean'},
'global-resources': {'type': 'boolean'}})
permissions = ('config:DescribeDeliveryChannels',
'config:DescribeConfigurationRecorders',
'config:DescribeConfigurationRecorderStatus')
[docs] def process(self, resources, event=None):
client = local_session(
self.manager.session_factory).client('config')
channels = client.describe_delivery_channels()[
'DeliveryChannels']
recorders = client.describe_configuration_recorders()[
'ConfigurationRecorders']
resources[0]['c7n:config_recorders'] = recorders
resources[0]['c7n:config_channels'] = channels
if self.data.get('global-resources'):
recorders = [
r for r in recorders
if r['recordingGroup'].get('includeGlobalResourceTypes')]
if self.data.get('all-resources'):
recorders = [r for r in recorders
if r['recordingGroup'].get('allSupported')]
if self.data.get('running', True) and recorders:
status = {s['name']: s for
s in client.describe_configuration_recorder_status(
)['ConfigurationRecordersStatus']}
resources[0]['c7n:config_status'] = status
recorders = [r for r in recorders if status[r['name']]['recording'] and
status[r['name']]['lastStatus'].lower() in ('pending', 'success')]
if channels and recorders:
return []
return resources
[docs]@filters.register('iam-summary')
class IAMSummary(ValueFilter):
"""Return annotated account resource if iam summary filter matches.
Some use cases include, detecting root api keys or mfa usage.
Example iam summary wrt to matchable fields::
{
"AccessKeysPerUserQuota": 2,
"AccountAccessKeysPresent": 0,
"AccountMFAEnabled": 1,
"AccountSigningCertificatesPresent": 0,
"AssumeRolePolicySizeQuota": 2048,
"AttachedPoliciesPerGroupQuota": 10,
"AttachedPoliciesPerRoleQuota": 10,
"AttachedPoliciesPerUserQuota": 10,
"GroupPolicySizeQuota": 5120,
"Groups": 1,
"GroupsPerUserQuota": 10,
"GroupsQuota": 100,
"InstanceProfiles": 0,
"InstanceProfilesQuota": 100,
"MFADevices": 3,
"MFADevicesInUse": 2,
"Policies": 3,
"PoliciesQuota": 1000,
"PolicySizeQuota": 5120,
"PolicyVersionsInUse": 5,
"PolicyVersionsInUseQuota": 10000,
"Providers": 0,
"RolePolicySizeQuota": 10240,
"Roles": 4,
"RolesQuota": 250,
"ServerCertificates": 0,
"ServerCertificatesQuota": 20,
"SigningCertificatesPerUserQuota": 2,
"UserPolicySizeQuota": 2048,
"Users": 5,
"UsersQuota": 5000,
"VersionsPerPolicyQuota": 5,
}
For example to determine if an account has either not been
enabled with root mfa or has root api keys.
.. code-block:: yaml
policies:
- name: root-keys-or-no-mfa
resource: account
filters:
- type: iam-summary
key: AccountMFAEnabled
value: true
op: eq
value_type: swap
"""
schema = type_schema('iam-summary', rinherit=ValueFilter.schema)
permissions = ('iam:GetAccountSummary',)
[docs] def process(self, resources, event=None):
if not resources[0].get('c7n:iam_summary'):
client = local_session(
self.manager.session_factory).client('iam')
resources[0]['c7n:iam_summary'] = client.get_account_summary(
)['SummaryMap']
if self.match(resources[0]['c7n:iam_summary']):
return resources
return []
[docs]@filters.register('password-policy')
class AccountPasswordPolicy(ValueFilter):
"""Check an account's password policy.
Note that on top of the default password policy fields, we also add an extra key,
PasswordPolicyConfigured which will be set to true or false to signify if the given
account has attempted to set a policy at all.
:example:
.. code-block:: yaml
policies:
- name: password-policy-check
resource: account
region: us-east-1
filters:
- type: password-policy
key: MinimumPasswordLength
value: 10
op: ge
- type: password-policy
key: RequireSymbols
value: true
"""
schema = type_schema('password-policy', rinherit=ValueFilter.schema)
permissions = ('iam:GetAccountPasswordPolicy',)
[docs] def process(self, resources, event=None):
account = resources[0]
if not account.get('c7n:password_policy'):
client = local_session(self.manager.session_factory).client('iam')
policy = {}
try:
policy = client.get_account_password_policy().get('PasswordPolicy', {})
policy['PasswordPolicyConfigured'] = True
except ClientError as e:
if e.response['Error']['Code'] == 'NoSuchEntity':
policy['PasswordPolicyConfigured'] = False
else:
raise
account['c7n:password_policy'] = policy
if self.match(account['c7n:password_policy']):
return resources
return []
[docs]@filters.register('service-limit')
class ServiceLimit(Filter):
"""Check if account's service limits are past a given threshold.
Supported limits are per trusted advisor, which is variable based
on usage in the account and support level enabled on the account.
- service: AutoScaling limit: Auto Scaling groups
- service: AutoScaling limit: Launch configurations
- service: EBS limit: Active snapshots
- service: EBS limit: Active volumes
- service: EBS limit: General Purpose (SSD) volume storage (GiB)
- service: EBS limit: Magnetic volume storage (GiB)
- service: EBS limit: Provisioned IOPS
- service: EBS limit: Provisioned IOPS (SSD) storage (GiB)
- service: EC2 limit: Elastic IP addresses (EIPs)
# Note this is extant for each active instance type in the account
# however the total value is against sum of all instance types.
# see issue https://github.com/cloud-custodian/cloud-custodian/issues/516
- service: EC2 limit: On-Demand instances - m3.medium
- service: EC2 limit: Reserved Instances - purchase limit (monthly)
- service: ELB limit: Active load balancers
- service: IAM limit: Groups
- service: IAM limit: Instance profiles
- service: IAM limit: Roles
- service: IAM limit: Server certificates
- service: IAM limit: Users
- service: RDS limit: DB instances
- service: RDS limit: DB parameter groups
- service: RDS limit: DB security groups
- service: RDS limit: DB snapshots per user
- service: RDS limit: Storage quota (GB)
- service: RDS limit: Internet gateways
- service: SES limit: Daily sending quota
- service: VPC limit: VPCs
- service: VPC limit: VPC Elastic IP addresses (EIPs)
:example:
.. code-block:: yaml
policies:
- name: account-service-limits
resource: account
filters:
- type: service-limit
services:
- EC2
threshold: 1.0
- name: specify-region-for-global-service
region: us-east-1
resource: account
filters:
- type: service-limit
services:
- IAM
limits:
- Roles
"""
schema = type_schema(
'service-limit',
threshold={'type': 'number'},
refresh_period={'type': 'integer'},
limits={'type': 'array', 'items': {'type': 'string'}},
services={'type': 'array', 'items': {
'enum': ['EC2', 'ELB', 'VPC', 'AutoScaling',
'RDS', 'EBS', 'SES', 'IAM']}})
permissions = ('support:DescribeTrustedAdvisorCheckResult',)
check_id = 'eW7HH0l7J9'
check_limit = ('region', 'service', 'check', 'limit', 'extant', 'color')
global_services = set(['IAM'])
[docs] def validate(self):
region = self.manager.data.get('region', '')
if len(self.global_services.intersection(self.data.get('services', []))):
if region != 'us-east-1':
raise PolicyValidationError(
"Global services: %s must be targeted in us-east-1 on the policy"
% ', '.join(self.global_services))
return self
[docs] def process(self, resources, event=None):
client = local_session(self.manager.session_factory).client(
'support', region_name='us-east-1')
checks = client.describe_trusted_advisor_check_result(
checkId=self.check_id, language='en')['result']
region = self.manager.config.region
checks['flaggedResources'] = [r for r in checks['flaggedResources']
if r['metadata'][0] == region or (r['metadata'][0] == '-' and region == 'us-east-1')]
resources[0]['c7n:ServiceLimits'] = checks
delta = timedelta(self.data.get('refresh_period', 1))
check_date = parse_date(checks['timestamp'])
if datetime.now(tz=tzutc()) - delta > check_date:
client.refresh_trusted_advisor_check(checkId=self.check_id)
threshold = self.data.get('threshold')
services = self.data.get('services')
limits = self.data.get('limits')
exceeded = []
for resource in checks['flaggedResources']:
if threshold is None and resource['status'] == 'ok':
continue
limit = dict(zip(self.check_limit, resource['metadata']))
if services and limit['service'] not in services:
continue
if limits and limit['check'] not in limits:
continue
limit['status'] = resource['status']
limit['percentage'] = float(limit['extant'] or 0) / float(
limit['limit']) * 100
if threshold and limit['percentage'] < threshold:
continue
exceeded.append(limit)
if exceeded:
resources[0]['c7n:ServiceLimitsExceeded'] = exceeded
return resources
return []
[docs]@actions.register('request-limit-increase')
class RequestLimitIncrease(BaseAction):
r"""File support ticket to raise limit.
:Example:
.. code-block:: yaml
policies:
- name: account-service-limits
resource: account
filters:
- type: service-limit
services:
- EBS
limits:
- Provisioned IOPS (SSD) storage (GiB)
threshold: 60.5
actions:
- type: request-limit-increase
notify: [email, email2]
## You can use one of either percent-increase or an amount-increase.
percent-increase: 50
message: "Please raise the below account limit(s); \n {limits}"
"""
schema = {
'type': 'object',
'notify': {'type': 'array'},
'properties': {
'type': {'enum': ['request-limit-increase']},
'percent-increase': {'type': 'number', 'minimum': 1},
'amount-increase': {'type': 'number', 'minimum': 1},
'minimum-increase': {'type': 'number', 'minimum': 1},
'subject': {'type': 'string'},
'message': {'type': 'string'},
'severity': {'type': 'string', 'enum': ['urgent', 'high', 'normal', 'low']}
},
'oneOf': [
{'required': ['type', 'percent-increase']},
{'required': ['type', 'amount-increase']}
]
}
permissions = ('support:CreateCase',)
default_subject = '[Account:{account}]Raise the following limit(s) of {service} in {region}'
default_template = 'Please raise the below account limit(s); \n {limits}'
default_severity = 'normal'
service_code_mapping = {
'AutoScaling': 'auto-scaling',
'ELB': 'elastic-load-balancing',
'EBS': 'amazon-elastic-block-store',
'EC2': 'amazon-elastic-compute-cloud-linux',
'RDS': 'amazon-relational-database-service-aurora',
'VPC': 'amazon-virtual-private-cloud',
'IAM': 'aws-identity-and-access-management',
'CloudFormation': 'aws-cloudformation',
'Kinesis': 'amazon-kinesis',
}
[docs] def process(self, resources):
session = local_session(self.manager.session_factory)
client = session.client('support', region_name='us-east-1')
account_id = self.manager.config.account_id
service_map = {}
region_map = {}
limit_exceeded = resources[0].get('c7n:ServiceLimitsExceeded', [])
percent_increase = self.data.get('percent-increase')
amount_increase = self.data.get('amount-increase')
minimum_increase = self.data.get('minimum-increase', 1)
for s in limit_exceeded:
current_limit = int(s['limit'])
if percent_increase:
increase_by = current_limit * float(percent_increase) / 100
increase_by = max(increase_by, minimum_increase)
else:
increase_by = amount_increase
increase_by = round(increase_by)
msg = '\nIncrease %s by %d in %s \n\t Current Limit: %s\n\t Current Usage: %s\n\t ' \
'Set New Limit to: %d' % (
s['check'], increase_by, s['region'], s['limit'], s['extant'],
(current_limit + increase_by))
service_map.setdefault(s['service'], []).append(msg)
region_map.setdefault(s['service'], s['region'])
for service in service_map:
subject = self.data.get('subject', self.default_subject).format(
service=service, region=region_map[service], account=account_id)
service_code = self.service_code_mapping.get(service)
body = self.data.get('message', self.default_template)
body = body.format(**{
'service': service,
'limits': '\n\t'.join(service_map[service]),
})
client.create_case(
subject=subject,
communicationBody=body,
serviceCode=service_code,
categoryCode='general-guidance',
severityCode=self.data.get('severity', self.default_severity),
ccEmailAddresses=self.data.get('notify', []))
[docs]def cloudtrail_policy(original, bucket_name, account_id, bucket_region):
'''add CloudTrail permissions to an S3 policy, preserving existing'''
ct_actions = [
{
'Action': 's3:GetBucketAcl',
'Effect': 'Allow',
'Principal': {'Service': 'cloudtrail.amazonaws.com'},
'Resource': generate_arn(
service='s3', resource=bucket_name, region=bucket_region),
'Sid': 'AWSCloudTrailAclCheck20150319',
},
{
'Action': 's3:PutObject',
'Condition': {
'StringEquals':
{'s3:x-amz-acl': 'bucket-owner-full-control'},
},
'Effect': 'Allow',
'Principal': {'Service': 'cloudtrail.amazonaws.com'},
'Resource': generate_arn(
service='s3', resource=bucket_name, region=bucket_region),
'Sid': 'AWSCloudTrailWrite20150319',
},
]
# parse original policy
if original is None:
policy = {
'Statement': [],
'Version': '2012-10-17',
}
else:
policy = json.loads(original['Policy'])
original_actions = [a.get('Action') for a in policy['Statement']]
for cta in ct_actions:
if cta['Action'] not in original_actions:
policy['Statement'].append(cta)
return json.dumps(policy)
# AWS Account doesn't participate in events (not based on query resource manager)
# so the event subscriber used by postfinding to register doesn't apply, manually
# register it.
Account.action_registry.register('post-finding', OtherResourcePostFinding)
[docs]@actions.register('enable-cloudtrail')
class EnableTrail(BaseAction):
"""Enables logging on the trail(s) named in the policy
:Example:
.. code-block:: yaml
policies:
- name: trail-test
description: Ensure CloudTrail logging is enabled
resource: account
actions:
- type: enable-cloudtrail
trail: mytrail
bucket: trails
"""
permissions = (
'cloudtrail:CreateTrail',
'cloudtrail:DescribeTrails',
'cloudtrail:GetTrailStatus',
'cloudtrail:StartLogging',
'cloudtrail:UpdateTrail',
's3:CreateBucket',
's3:GetBucketPolicy',
's3:PutBucketPolicy',
)
schema = type_schema(
'enable-cloudtrail',
**{
'trail': {'type': 'string'},
'bucket': {'type': 'string'},
'bucket-region': {'type': 'string'},
'multi-region': {'type': 'boolean'},
'global-events': {'type': 'boolean'},
'notify': {'type': 'string'},
'file-digest': {'type': 'boolean'},
'kms': {'type': 'boolean'},
'kms-key': {'type': 'string'},
'required': ('bucket',),
}
)
[docs] def process(self, accounts):
"""Create or enable CloudTrail"""
session = local_session(self.manager.session_factory)
client = session.client('cloudtrail')
bucket_name = self.data['bucket']
bucket_region = self.data.get('bucket-region', 'us-east-1')
trail_name = self.data.get('trail', 'default-trail')
multi_region = self.data.get('multi-region', True)
global_events = self.data.get('global-events', True)
notify = self.data.get('notify', '')
file_digest = self.data.get('file-digest', False)
kms = self.data.get('kms', False)
kms_key = self.data.get('kms-key', '')
s3client = session.client('s3', region_name=bucket_region)
try:
s3client.create_bucket(
Bucket=bucket_name,
CreateBucketConfiguration={'LocationConstraint': bucket_region}
)
except ClientError as ce:
if not ('Error' in ce.response and
ce.response['Error']['Code'] == 'BucketAlreadyOwnedByYou'):
raise ce
try:
current_policy = s3client.get_bucket_policy(Bucket=bucket_name)
except ClientError:
current_policy = None
policy_json = cloudtrail_policy(
current_policy, bucket_name,
self.manager.config.account_id, bucket_region)
s3client.put_bucket_policy(Bucket=bucket_name, Policy=policy_json)
trails = client.describe_trails().get('trailList', ())
if trail_name not in [t.get('Name') for t in trails]:
new_trail = client.create_trail(
Name=trail_name,
S3BucketName=bucket_name,
)
if new_trail:
trails.append(new_trail)
# the loop below will configure the new trail
for trail in trails:
if trail.get('Name') != trail_name:
continue
# enable
arn = trail['TrailARN']
status = client.get_trail_status(Name=arn)
if not status['IsLogging']:
client.start_logging(Name=arn)
# apply configuration changes (if any)
update_args = {}
if multi_region != trail.get('IsMultiRegionTrail'):
update_args['IsMultiRegionTrail'] = multi_region
if global_events != trail.get('IncludeGlobalServiceEvents'):
update_args['IncludeGlobalServiceEvents'] = global_events
if notify != trail.get('SNSTopicArn'):
update_args['SnsTopicName'] = notify
if file_digest != trail.get('LogFileValidationEnabled'):
update_args['EnableLogFileValidation'] = file_digest
if kms_key != trail.get('KmsKeyId'):
if not kms and 'KmsKeyId' in trail:
kms_key = ''
update_args['KmsKeyId'] = kms_key
if update_args:
update_args['Name'] = trail_name
client.update_trail(**update_args)
[docs]@filters.register('has-virtual-mfa')
class HasVirtualMFA(Filter):
"""Is the account configured with a virtual MFA device?
:example:
.. code-block:: yaml
policies:
- name: account-with-virtual-mfa
resource: account
region: us-east-1
filters:
- type: has-virtual-mfa
value: true
"""
schema = type_schema('has-virtual-mfa', **{'value': {'type': 'boolean'}})
permissions = ('iam:ListVirtualMFADevices',)
[docs] def mfa_belongs_to_root_account(self, mfa):
return mfa['SerialNumber'].endswith(':mfa/root-account-mfa-device')
[docs] def account_has_virtual_mfa(self, account):
if not account.get('c7n:VirtualMFADevices'):
client = local_session(self.manager.session_factory).client('iam')
paginator = client.get_paginator('list_virtual_mfa_devices')
raw_list = paginator.paginate().build_full_result()['VirtualMFADevices']
account['c7n:VirtualMFADevices'] = list(filter(
self.mfa_belongs_to_root_account, raw_list))
expect_virtual_mfa = self.data.get('value', True)
has_virtual_mfa = any(account['c7n:VirtualMFADevices'])
return expect_virtual_mfa == has_virtual_mfa
[docs] def process(self, resources, event=None):
return list(filter(self.account_has_virtual_mfa, resources))
[docs]@actions.register('enable-data-events')
class EnableDataEvents(BaseAction):
"""Ensure all buckets in account are setup to log data events.
Note this works via a single trail for data events per
https://aws.amazon.com/about-aws/whats-new/2017/09/aws-cloudtrail-enables-option-to-add-all-amazon-s3-buckets-to-data-events/
This trail should NOT be used for api management events, the
configuration here is soley for data events. If directed to create
a trail this will do so without management events.
:example:
.. code-block:: yaml
policies:
- name: s3-enable-data-events-logging
resource: account
actions:
- type: enable-data-events
data-trail:
name: s3-events
multi-region: us-east-1
"""
schema = type_schema(
'enable-data-events', required=['data-trail'], **{
'data-trail': {
'type': 'object',
'additionalProperties': False,
'required': ['name'],
'properties': {
'create': {
'title': 'Should we create trail if needed for events?',
'type': 'boolean'},
'type': {'enum': ['ReadOnly', 'WriteOnly', 'All']},
'name': {
'title': 'The name of the event trail',
'type': 'string'},
'topic': {
'title': 'If creating, the sns topic for the trail to send updates',
'type': 'string'},
's3-bucket': {
'title': 'If creating, the bucket to store trail event data',
'type': 'string'},
's3-prefix': {'type': 'string'},
'key-id': {
'title': 'If creating, Enable kms on the trail',
'type': 'string'},
# region that we're aggregating via trails.
'multi-region': {
'title': 'If creating, use this region for all data trails',
'type': 'string'}}}})
[docs] def validate(self):
if self.data['data-trail'].get('create'):
if 's3-bucket' not in self.data['data-trail']:
raise PolicyValidationError(
"If creating data trails, an s3-bucket is required on %s" % (
self.manager.data))
return self
[docs] def get_permissions(self):
perms = [
'cloudtrail:DescribeTrails',
'cloudtrail:GetEventSelectors',
'cloudtrail:PutEventSelectors']
if self.data.get('data-trail', {}).get('create'):
perms.extend([
'cloudtrail:CreateTrail', 'cloudtrail:StartLogging'])
return perms
[docs] def add_data_trail(self, client, trail_cfg):
if not trail_cfg.get('create'):
raise ValueError(
"s3 data event trail missing and not configured to create")
params = dict(
Name=trail_cfg['name'],
S3BucketName=trail_cfg['s3-bucket'],
EnableLogFileValidation=True)
if 'key-id' in trail_cfg:
params['KmsKeyId'] = trail_cfg['key-id']
if 's3-prefix' in trail_cfg:
params['S3KeyPrefix'] = trail_cfg['s3-prefix']
if 'topic' in trail_cfg:
params['SnsTopicName'] = trail_cfg['topic']
if 'multi-region' in trail_cfg:
params['IsMultiRegionTrail'] = True
client.create_trail(**params)
return {'Name': trail_cfg['name']}
[docs] def process(self, resources):
session = local_session(self.manager.session_factory)
region = self.data['data-trail'].get('multi-region')
if region:
client = session.client('cloudtrail', region_name=region)
else:
client = session.client('cloudtrail')
added = False
tconfig = self.data['data-trail']
trails = client.describe_trails(
trailNameList=[tconfig['name']]).get('trailList', ())
if not trails:
trail = self.add_data_trail(client, tconfig)
added = True
else:
trail = trails[0]
events = client.get_event_selectors(
TrailName=trail['Name']).get('EventSelectors', [])
for e in events:
found = False
if not e.get('DataResources'):
continue
for data_events in e['DataResources']:
if data_events['Type'] != 'AWS::S3::Object':
continue
for b in data_events['Values']:
if b.rsplit(':')[-1].strip('/') == '':
found = True
break
if found:
resources[0]['c7n_data_trail'] = trail
return
# Opinionated choice, separate api and data events.
event_count = len(events)
events = [e for e in events if not e.get('IncludeManagementEvents')]
if len(events) != event_count:
self.log.warning("removing api trail from data trail")
# future proof'd for other data events, for s3 this trail
# encompasses all the buckets in the account.
events.append({
'IncludeManagementEvents': False,
'ReadWriteType': tconfig.get('type', 'All'),
'DataResources': [{
'Type': 'AWS::S3::Object',
'Values': ['arn:aws:s3:::']}]})
client.put_event_selectors(
TrailName=trail['Name'],
EventSelectors=events)
if added:
client.start_logging(Name=tconfig['name'])
resources[0]['c7n_data_trail'] = trail
[docs]@filters.register('shield-enabled')
class ShieldEnabled(Filter):
permissions = ('shield:DescribeSubscription',)
schema = type_schema(
'shield-enabled',
state={'type': 'boolean'})
[docs] def process(self, resources, event=None):
state = self.data.get('state', False)
client = local_session(self.manager.session_factory).client('shield')
try:
subscription = client.describe_subscription().get(
'Subscription', None)
except ClientError as e:
if e.response['Error']['Code'] != 'ResourceNotFoundException':
raise
subscription = None
resources[0]['c7n:ShieldSubscription'] = subscription
if state and subscription:
return resources
elif not state and not subscription:
return resources
return []
[docs]@actions.register('set-shield-advanced')
class SetShieldAdvanced(BaseAction):
"""Enable/disable Shield Advanced on an account."""
permissions = (
'shield:CreateSubscription', 'shield:DeleteSubscription')
schema = type_schema(
'set-shield-advanced',
state={'type': 'boolean'})
[docs] def process(self, resources):
client = local_session(self.manager.session_factory).client('shield')
state = self.data.get('state', True)
if state:
client.create_subscription()
else:
try:
client.delete_subscription()
except ClientError as e:
if e.response['Error']['Code'] == 'ResourceNotFoundException':
return
raise
[docs]@filters.register('xray-encrypt-key')
class XrayEncrypted(Filter):
"""Determine if xray is encrypted.
:example:
.. code-block:: yaml
policies:
- name: xray-encrypt-with-default
resource: aws.account
filters:
- type: xray-encrypt-key
key: default
- name: xray-encrypt-with-kms
resource: aws.account
filters:
- type: xray-encrypt-key
key: kms
- name: xray-encrypt-with-specific-key
resource: aws.account
filters:
- type: xray-encrypt-key
key: alias/my-alias or arn or keyid
"""
permissions = ('xray:GetEncryptionConfig',)
schema = type_schema(
'xray-encrypt-key',
required=['key'],
key={'type': 'string'}
)
[docs] def process(self, resources, event=None):
client = self.manager.session_factory().client('xray')
gec_result = client.get_encryption_config()['EncryptionConfig']
resources[0]['c7n:XrayEncryptionConfig'] = gec_result
k = self.data.get('key')
if k not in ['default', 'kms']:
kmsclient = self.manager.session_factory().client('kms')
keyid = kmsclient.describe_key(KeyId=k)['KeyMetadata']['Arn']
rc = resources if (gec_result['KeyId'] == keyid) else []
else:
kv = 'KMS' if self.data.get('key') == 'kms' else 'NONE'
rc = resources if (gec_result['Type'] == kv) else []
return rc
[docs]@actions.register('set-xray-encrypt')
class SetXrayEncryption(BaseAction):
"""Enable specific xray encryption.
:example:
.. code-block:: yaml
policies:
- name: xray-default-encrypt
resource: aws.account
actions:
- type: set-xray-encrypt
key: default
- name: xray-kms-encrypt
resource: aws.account
actions:
- type: set-xray-encrypt
key: alias/some/alias/key
"""
permissions = ('xray:PutEncryptionConfig',)
schema = type_schema(
'set-xray-encrypt',
required=['key'],
key={'type': 'string'}
)
[docs] def process(self, resources):
client = local_session(self.manager.session_factory).client('xray')
key = self.data.get('key')
req = {'Type': 'NONE'} if key == 'default' else {'Type': 'KMS', 'KeyId': key}
client.put_encryption_config(**req)
[docs]@filters.register('s3-public-block')
class S3PublicBlock(ValueFilter):
"""Check for s3 public blocks on an account.
https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html
"""
annotation_key = 'c7n:s3-public-block'
annotate = False # no annotation from value filter
schema = type_schema('s3-public-block', rinherit=ValueFilter.schema)
permissions = ('s3:GetAccountPublicAccessBlock',)
[docs] def process(self, resources, event=None):
self.augment([r for r in resources if self.annotation_key not in r])
return super(S3PublicBlock, self).process(resources, event)
[docs] def augment(self, resources):
client = local_session(self.manager.session_factory).client('s3control')
for r in resources:
try:
r[self.annotation_key] = client.get_public_access_block(
AccountId=r['account_id']).get('PublicAccessBlockConfiguration', {})
except client.exceptions.NoSuchPublicAccessBlockConfiguration:
r[self.annotation_key] = {}
def __call__(self, r):
return super(S3PublicBlock, self).__call__(r[self.annotation_key])
[docs]@actions.register('set-s3-public-block')
class SetS3PublicBlock(BaseAction):
"""Configure S3 Public Access Block on an account.
All public access block attributes can be set. If not specified they are merged
with the extant configuration.
https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html
:example:
.. yaml:
policies:
- name: restrict-public-buckets
resource: aws.account
filters:
- not:
- type: s3-public-block
key: RestrictPublicBuckets
value: true
actions:
- type: set-s3-public-block
RestrictPublicBuckets: true
"""
schema = type_schema(
'set-s3-public-block',
state={'type': 'boolean', 'default': True},
BlockPublicAcls={'type': 'boolean'},
IgnorePublicAcls={'type': 'boolean'},
BlockPublicPolicy={'type': 'boolean'},
RestrictPublicBuckets={'type': 'boolean'})
permissions = ('s3:PutAccountPublicAccessBlock', 's3:GetAccountPublicAccessBlock')
[docs] def validate(self):
config = self.data.copy()
config.pop('type')
if config.pop('state', None) is False and config:
raise PolicyValidationError(
"%s cant set state false with controls specified".format(
self.type))
[docs] def process(self, resources):
client = local_session(self.manager.session_factory).client('s3control')
if self.data.get('state', True) is False:
for r in resources:
client.delete_public_access_block(AccountId=r['account_id'])
return
keys = (
'BlockPublicPolicy', 'BlockPublicAcls', 'IgnorePublicAcls', 'RestrictPublicBuckets')
for r in resources:
# try to merge with existing configuration if not explicitly set.
base = {}
if S3PublicBlock.annotation_key in r:
base = r[S3PublicBlock.annotation_key]
else:
try:
base = client.get_public_access_block(AccountId=r['account_id']).get(
'PublicAccessBlockConfiguration')
except client.exceptions.NoSuchPublicAccessBlockConfiguration:
base = {}
config = {}
for k in keys:
if k in self.data:
config[k] = self.data[k]
elif k in base:
config[k] = base[k]
client.put_public_access_block(
AccountId=r['account_id'],
PublicAccessBlockConfiguration=config)