Path: blob/develop/awscli/customizations/cloudtrail/subscribe.py
1567 views
# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.1#2# Licensed under the Apache License, Version 2.0 (the "License"). You3# may not use this file except in compliance with the License. A copy of4# the License is located at5#6# http://aws.amazon.com/apache2.0/7#8# or in the "license" file accompanying this file. This file is9# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF10# ANY KIND, either express or implied. See the License for the specific11# language governing permissions and limitations under the License.12import json13import logging14import sys1516from .utils import get_account_id17from awscli.customizations.commands import BasicCommand18from awscli.customizations.utils import s3_bucket_exists19from awscli.utils import create_nested_client20from botocore.exceptions import ClientError2122LOG = logging.getLogger(__name__)23S3_POLICY_TEMPLATE = 'policy/S3/AWSCloudTrail-S3BucketPolicy-2014-12-17.json'24SNS_POLICY_TEMPLATE = 'policy/SNS/AWSCloudTrail-SnsTopicPolicy-2014-12-17.json'252627class CloudTrailError(Exception):28pass293031class CloudTrailSubscribe(BasicCommand):32"""33Subscribe/update a user account to CloudTrail, creating the required S3 bucket,34the optional SNS topic, and starting the CloudTrail monitoring and logging.35"""36NAME = 'create-subscription'37DESCRIPTION = ('Creates and configures the AWS resources necessary to use'38' CloudTrail, creates a trail using those resources, and '39'turns on logging.')40SYNOPSIS = ('aws cloudtrail create-subscription'41' (--s3-use-bucket|--s3-new-bucket) bucket-name'42' [--sns-new-topic topic-name]\n')4344ARG_TABLE = [45{'name': 'name', 'required': True, 'help_text': 'Cloudtrail name'},46{'name': 's3-new-bucket',47'help_text': 'Create a new S3 bucket with this name'},48{'name': 's3-use-bucket',49'help_text': 'Use an existing S3 bucket with this name'},50{'name': 's3-prefix', 'help_text': 'S3 object prefix'},51{'name': 'sns-new-topic',52'help_text': 'Create a new SNS topic with this name'},53{'name': 'include-global-service-events',54'help_text': 'Whether to include global service events'},55{'name': 's3-custom-policy',56'help_text': 'Custom S3 policy template or URL'},57{'name': 'sns-custom-policy',58'help_text': 'Custom SNS policy template or URL'}59]60UPDATE = False61_UNDOCUMENTED = True6263def _run_main(self, args, parsed_globals):64self.setup_services(args, parsed_globals)65# Run the command and report success66self._call(args, parsed_globals)6768return 06970def setup_services(self, args, parsed_globals):71client_args = {72'region_name': None,73'verify': None74}75if parsed_globals.region is not None:76client_args['region_name'] = parsed_globals.region77if parsed_globals.verify_ssl is not None:78client_args['verify'] = parsed_globals.verify_ssl7980# Initialize services81LOG.debug('Initializing S3, SNS and CloudTrail...')82self.sts = create_nested_client(self._session, 'sts', **client_args)83self.s3 = create_nested_client(self._session, 's3', **client_args)84self.sns = create_nested_client(self._session, 'sns', **client_args)85self.region_name = self.s3.meta.region_name8687# If the endpoint is specified, it is designated for the cloudtrail88# service. Not all of the other services will use it.89if parsed_globals.endpoint_url is not None:90client_args['endpoint_url'] = parsed_globals.endpoint_url91self.cloudtrail = create_nested_client(self._session, 'cloudtrail', **client_args)9293def _call(self, options, parsed_globals):94"""95Run the command. Calls various services based on input options and96outputs the final CloudTrail configuration.97"""98gse = options.include_global_service_events99if gse:100if gse.lower() == 'true':101gse = True102elif gse.lower() == 'false':103gse = False104else:105raise ValueError('You must pass either true or false to'106' --include-global-service-events.')107108bucket = options.s3_use_bucket109110if options.s3_new_bucket:111bucket = options.s3_new_bucket112113if self.UPDATE and options.s3_prefix is None:114# Prefix was not passed and this is updating the S3 bucket,115# so let's find the existing prefix and use that if possible116res = self.cloudtrail.describe_trails(117trailNameList=[options.name])118trail_info = res['trailList'][0]119120if 'S3KeyPrefix' in trail_info:121LOG.debug('Setting S3 prefix to {0}'.format(122trail_info['S3KeyPrefix']))123options.s3_prefix = trail_info['S3KeyPrefix']124125self.setup_new_bucket(bucket, options.s3_prefix,126options.s3_custom_policy)127elif not bucket and not self.UPDATE:128# No bucket was passed for creation.129raise ValueError('You must pass either --s3-use-bucket or'130' --s3-new-bucket to create.')131132if options.sns_new_topic:133try:134topic_result = self.setup_new_topic(options.sns_new_topic,135options.sns_custom_policy)136except Exception:137# Roll back any S3 bucket creation138if options.s3_new_bucket:139self.s3.delete_bucket(Bucket=options.s3_new_bucket)140raise141142try:143cloudtrail_config = self.upsert_cloudtrail_config(144options.name,145bucket,146options.s3_prefix,147options.sns_new_topic,148gse149)150except Exception:151# Roll back any S3 bucket / SNS topic creations152if options.s3_new_bucket:153self.s3.delete_bucket(Bucket=options.s3_new_bucket)154if options.sns_new_topic:155self.sns.delete_topic(TopicArn=topic_result['TopicArn'])156raise157158sys.stdout.write('CloudTrail configuration:\n{config}\n'.format(159config=json.dumps(cloudtrail_config, indent=2)))160161if not self.UPDATE:162# If the configure call command above completes then this should163# have a really high chance of also completing164self.start_cloudtrail(options.name)165166sys.stdout.write(167'Logs will be delivered to {bucket}:{prefix}\n'.format(168bucket=bucket, prefix=options.s3_prefix or ''))169170def _get_policy(self, key_name):171try:172data = self.s3.get_object(173Bucket='awscloudtrail-policy-' + self.region_name,174Key=key_name)175return data['Body'].read().decode('utf-8')176except Exception as e:177raise CloudTrailError(178'Unable to get regional policy template for'179' region %s: %s. Error: %s', self.region_name, key_name, e)180181def setup_new_bucket(self, bucket, prefix, custom_policy=None):182"""183Creates a new S3 bucket with an appropriate policy to let CloudTrail184write to the prefix path.185"""186sys.stdout.write(187'Setting up new S3 bucket {bucket}...\n'.format(bucket=bucket))188189account_id = get_account_id(self.sts)190191# Clean up the prefix - it requires a trailing slash if set192if prefix and not prefix.endswith('/'):193prefix += '/'194195# Fetch policy data from S3 or a custom URL196if custom_policy is not None:197policy = custom_policy198else:199policy = self._get_policy(S3_POLICY_TEMPLATE)200201policy = policy.replace('<BucketName>', bucket)\202.replace('<CustomerAccountID>', account_id)203204if '<Prefix>/' in policy:205policy = policy.replace('<Prefix>/', prefix or '')206else:207policy = policy.replace('<Prefix>', prefix or '')208209LOG.debug('Bucket policy:\n{0}'.format(policy))210bucket_exists = s3_bucket_exists(self.s3, bucket)211if bucket_exists:212raise Exception('Bucket {bucket} already exists.'.format(213bucket=bucket))214215# If we are not using the us-east-1 region, then we must set216# a location constraint on the new bucket.217params = {'Bucket': bucket}218if self.region_name != 'us-east-1':219bucket_config = {'LocationConstraint': self.region_name}220params['CreateBucketConfiguration'] = bucket_config221222data = self.s3.create_bucket(**params)223224try:225self.s3.put_bucket_policy(Bucket=bucket, Policy=policy)226except ClientError:227# Roll back bucket creation.228self.s3.delete_bucket(Bucket=bucket)229raise230231return data232233def setup_new_topic(self, topic, custom_policy=None):234"""235Creates a new SNS topic with an appropriate policy to let CloudTrail236post messages to the topic.237"""238sys.stdout.write(239'Setting up new SNS topic {topic}...\n'.format(topic=topic))240241account_id = get_account_id(self.sts)242243# Make sure topic doesn't already exist244# Warn but do not fail if ListTopics permissions245# are missing from the IAM role?246try:247topics = self.sns.list_topics()['Topics']248except Exception:249topics = []250LOG.warn('Unable to list topics, continuing...')251252if [t for t in topics if t['TopicArn'].split(':')[-1] == topic]:253raise Exception('Topic {topic} already exists.'.format(254topic=topic))255256region = self.sns.meta.region_name257258# Get the SNS topic policy information to allow CloudTrail259# write-access.260if custom_policy is not None:261policy = custom_policy262else:263policy = self._get_policy(SNS_POLICY_TEMPLATE)264265policy = policy.replace('<Region>', region)\266.replace('<SNSTopicOwnerAccountId>', account_id)\267.replace('<SNSTopicName>', topic)268269topic_result = self.sns.create_topic(Name=topic)270271try:272# Merge any existing topic policy with our new policy statements273topic_attr = self.sns.get_topic_attributes(274TopicArn=topic_result['TopicArn'])275276policy = self.merge_sns_policy(topic_attr['Attributes']['Policy'],277policy)278279LOG.debug('Topic policy:\n{0}'.format(policy))280281# Set the topic policy282self.sns.set_topic_attributes(TopicArn=topic_result['TopicArn'],283AttributeName='Policy',284AttributeValue=policy)285except Exception:286# Roll back topic creation287self.sns.delete_topic(TopicArn=topic_result['TopicArn'])288raise289290return topic_result291292def merge_sns_policy(self, left, right):293"""294Merge two SNS topic policy documents. The id information from295``left`` is used in the final document, and the statements296from ``right`` are merged into ``left``.297298http://docs.aws.amazon.com/sns/latest/dg/BasicStructure.html299300:type left: string301:param left: First policy JSON document302:type right: string303:param right: Second policy JSON document304:rtype: string305:return: Merged policy JSON306"""307left_parsed = json.loads(left)308right_parsed = json.loads(right)309left_parsed['Statement'] += right_parsed['Statement']310return json.dumps(left_parsed)311312def upsert_cloudtrail_config(self, name, bucket, prefix, topic, gse):313"""314Either create or update the CloudTrail configuration depending on315whether this command is a create or update command.316"""317sys.stdout.write('Creating/updating CloudTrail configuration...\n')318config = {319'Name': name320}321if bucket is not None:322config['S3BucketName'] = bucket323if prefix is not None:324config['S3KeyPrefix'] = prefix325if topic is not None:326config['SnsTopicName'] = topic327if gse is not None:328config['IncludeGlobalServiceEvents'] = gse329if not self.UPDATE:330self.cloudtrail.create_trail(**config)331else:332self.cloudtrail.update_trail(**config)333return self.cloudtrail.describe_trails()334335def start_cloudtrail(self, name):336"""337Start the CloudTrail service, which begins logging.338"""339sys.stdout.write('Starting CloudTrail service...\n')340return self.cloudtrail.start_logging(Name=name)341342343class CloudTrailUpdate(CloudTrailSubscribe):344"""345Like subscribe above, but the update version of the command.346"""347NAME = 'update-subscription'348UPDATE = True349350DESCRIPTION = ('Updates any of the trail configuration settings, and'351' creates and configures any new AWS resources specified.')352353SYNOPSIS = ('aws cloudtrail update-subscription'354' [(--s3-use-bucket|--s3-new-bucket) bucket-name]'355' [--sns-new-topic topic-name]\n')356357358