Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aws
GitHub Repository: aws/aws-cli
Path: blob/develop/awscli/customizations/cloudtrail/subscribe.py
1567 views
1
# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
#
3
# Licensed under the Apache License, Version 2.0 (the "License"). You
4
# may not use this file except in compliance with the License. A copy of
5
# the License is located at
6
#
7
# http://aws.amazon.com/apache2.0/
8
#
9
# or in the "license" file accompanying this file. This file is
10
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
# ANY KIND, either express or implied. See the License for the specific
12
# language governing permissions and limitations under the License.
13
import json
14
import logging
15
import sys
16
17
from .utils import get_account_id
18
from awscli.customizations.commands import BasicCommand
19
from awscli.customizations.utils import s3_bucket_exists
20
from awscli.utils import create_nested_client
21
from botocore.exceptions import ClientError
22
23
LOG = logging.getLogger(__name__)
24
S3_POLICY_TEMPLATE = 'policy/S3/AWSCloudTrail-S3BucketPolicy-2014-12-17.json'
25
SNS_POLICY_TEMPLATE = 'policy/SNS/AWSCloudTrail-SnsTopicPolicy-2014-12-17.json'
26
27
28
class CloudTrailError(Exception):
29
pass
30
31
32
class CloudTrailSubscribe(BasicCommand):
33
"""
34
Subscribe/update a user account to CloudTrail, creating the required S3 bucket,
35
the optional SNS topic, and starting the CloudTrail monitoring and logging.
36
"""
37
NAME = 'create-subscription'
38
DESCRIPTION = ('Creates and configures the AWS resources necessary to use'
39
' CloudTrail, creates a trail using those resources, and '
40
'turns on logging.')
41
SYNOPSIS = ('aws cloudtrail create-subscription'
42
' (--s3-use-bucket|--s3-new-bucket) bucket-name'
43
' [--sns-new-topic topic-name]\n')
44
45
ARG_TABLE = [
46
{'name': 'name', 'required': True, 'help_text': 'Cloudtrail name'},
47
{'name': 's3-new-bucket',
48
'help_text': 'Create a new S3 bucket with this name'},
49
{'name': 's3-use-bucket',
50
'help_text': 'Use an existing S3 bucket with this name'},
51
{'name': 's3-prefix', 'help_text': 'S3 object prefix'},
52
{'name': 'sns-new-topic',
53
'help_text': 'Create a new SNS topic with this name'},
54
{'name': 'include-global-service-events',
55
'help_text': 'Whether to include global service events'},
56
{'name': 's3-custom-policy',
57
'help_text': 'Custom S3 policy template or URL'},
58
{'name': 'sns-custom-policy',
59
'help_text': 'Custom SNS policy template or URL'}
60
]
61
UPDATE = False
62
_UNDOCUMENTED = True
63
64
def _run_main(self, args, parsed_globals):
65
self.setup_services(args, parsed_globals)
66
# Run the command and report success
67
self._call(args, parsed_globals)
68
69
return 0
70
71
def setup_services(self, args, parsed_globals):
72
client_args = {
73
'region_name': None,
74
'verify': None
75
}
76
if parsed_globals.region is not None:
77
client_args['region_name'] = parsed_globals.region
78
if parsed_globals.verify_ssl is not None:
79
client_args['verify'] = parsed_globals.verify_ssl
80
81
# Initialize services
82
LOG.debug('Initializing S3, SNS and CloudTrail...')
83
self.sts = create_nested_client(self._session, 'sts', **client_args)
84
self.s3 = create_nested_client(self._session, 's3', **client_args)
85
self.sns = create_nested_client(self._session, 'sns', **client_args)
86
self.region_name = self.s3.meta.region_name
87
88
# If the endpoint is specified, it is designated for the cloudtrail
89
# service. Not all of the other services will use it.
90
if parsed_globals.endpoint_url is not None:
91
client_args['endpoint_url'] = parsed_globals.endpoint_url
92
self.cloudtrail = create_nested_client(self._session, 'cloudtrail', **client_args)
93
94
def _call(self, options, parsed_globals):
95
"""
96
Run the command. Calls various services based on input options and
97
outputs the final CloudTrail configuration.
98
"""
99
gse = options.include_global_service_events
100
if gse:
101
if gse.lower() == 'true':
102
gse = True
103
elif gse.lower() == 'false':
104
gse = False
105
else:
106
raise ValueError('You must pass either true or false to'
107
' --include-global-service-events.')
108
109
bucket = options.s3_use_bucket
110
111
if options.s3_new_bucket:
112
bucket = options.s3_new_bucket
113
114
if self.UPDATE and options.s3_prefix is None:
115
# Prefix was not passed and this is updating the S3 bucket,
116
# so let's find the existing prefix and use that if possible
117
res = self.cloudtrail.describe_trails(
118
trailNameList=[options.name])
119
trail_info = res['trailList'][0]
120
121
if 'S3KeyPrefix' in trail_info:
122
LOG.debug('Setting S3 prefix to {0}'.format(
123
trail_info['S3KeyPrefix']))
124
options.s3_prefix = trail_info['S3KeyPrefix']
125
126
self.setup_new_bucket(bucket, options.s3_prefix,
127
options.s3_custom_policy)
128
elif not bucket and not self.UPDATE:
129
# No bucket was passed for creation.
130
raise ValueError('You must pass either --s3-use-bucket or'
131
' --s3-new-bucket to create.')
132
133
if options.sns_new_topic:
134
try:
135
topic_result = self.setup_new_topic(options.sns_new_topic,
136
options.sns_custom_policy)
137
except Exception:
138
# Roll back any S3 bucket creation
139
if options.s3_new_bucket:
140
self.s3.delete_bucket(Bucket=options.s3_new_bucket)
141
raise
142
143
try:
144
cloudtrail_config = self.upsert_cloudtrail_config(
145
options.name,
146
bucket,
147
options.s3_prefix,
148
options.sns_new_topic,
149
gse
150
)
151
except Exception:
152
# Roll back any S3 bucket / SNS topic creations
153
if options.s3_new_bucket:
154
self.s3.delete_bucket(Bucket=options.s3_new_bucket)
155
if options.sns_new_topic:
156
self.sns.delete_topic(TopicArn=topic_result['TopicArn'])
157
raise
158
159
sys.stdout.write('CloudTrail configuration:\n{config}\n'.format(
160
config=json.dumps(cloudtrail_config, indent=2)))
161
162
if not self.UPDATE:
163
# If the configure call command above completes then this should
164
# have a really high chance of also completing
165
self.start_cloudtrail(options.name)
166
167
sys.stdout.write(
168
'Logs will be delivered to {bucket}:{prefix}\n'.format(
169
bucket=bucket, prefix=options.s3_prefix or ''))
170
171
def _get_policy(self, key_name):
172
try:
173
data = self.s3.get_object(
174
Bucket='awscloudtrail-policy-' + self.region_name,
175
Key=key_name)
176
return data['Body'].read().decode('utf-8')
177
except Exception as e:
178
raise CloudTrailError(
179
'Unable to get regional policy template for'
180
' region %s: %s. Error: %s', self.region_name, key_name, e)
181
182
def setup_new_bucket(self, bucket, prefix, custom_policy=None):
183
"""
184
Creates a new S3 bucket with an appropriate policy to let CloudTrail
185
write to the prefix path.
186
"""
187
sys.stdout.write(
188
'Setting up new S3 bucket {bucket}...\n'.format(bucket=bucket))
189
190
account_id = get_account_id(self.sts)
191
192
# Clean up the prefix - it requires a trailing slash if set
193
if prefix and not prefix.endswith('/'):
194
prefix += '/'
195
196
# Fetch policy data from S3 or a custom URL
197
if custom_policy is not None:
198
policy = custom_policy
199
else:
200
policy = self._get_policy(S3_POLICY_TEMPLATE)
201
202
policy = policy.replace('<BucketName>', bucket)\
203
.replace('<CustomerAccountID>', account_id)
204
205
if '<Prefix>/' in policy:
206
policy = policy.replace('<Prefix>/', prefix or '')
207
else:
208
policy = policy.replace('<Prefix>', prefix or '')
209
210
LOG.debug('Bucket policy:\n{0}'.format(policy))
211
bucket_exists = s3_bucket_exists(self.s3, bucket)
212
if bucket_exists:
213
raise Exception('Bucket {bucket} already exists.'.format(
214
bucket=bucket))
215
216
# If we are not using the us-east-1 region, then we must set
217
# a location constraint on the new bucket.
218
params = {'Bucket': bucket}
219
if self.region_name != 'us-east-1':
220
bucket_config = {'LocationConstraint': self.region_name}
221
params['CreateBucketConfiguration'] = bucket_config
222
223
data = self.s3.create_bucket(**params)
224
225
try:
226
self.s3.put_bucket_policy(Bucket=bucket, Policy=policy)
227
except ClientError:
228
# Roll back bucket creation.
229
self.s3.delete_bucket(Bucket=bucket)
230
raise
231
232
return data
233
234
def setup_new_topic(self, topic, custom_policy=None):
235
"""
236
Creates a new SNS topic with an appropriate policy to let CloudTrail
237
post messages to the topic.
238
"""
239
sys.stdout.write(
240
'Setting up new SNS topic {topic}...\n'.format(topic=topic))
241
242
account_id = get_account_id(self.sts)
243
244
# Make sure topic doesn't already exist
245
# Warn but do not fail if ListTopics permissions
246
# are missing from the IAM role?
247
try:
248
topics = self.sns.list_topics()['Topics']
249
except Exception:
250
topics = []
251
LOG.warn('Unable to list topics, continuing...')
252
253
if [t for t in topics if t['TopicArn'].split(':')[-1] == topic]:
254
raise Exception('Topic {topic} already exists.'.format(
255
topic=topic))
256
257
region = self.sns.meta.region_name
258
259
# Get the SNS topic policy information to allow CloudTrail
260
# write-access.
261
if custom_policy is not None:
262
policy = custom_policy
263
else:
264
policy = self._get_policy(SNS_POLICY_TEMPLATE)
265
266
policy = policy.replace('<Region>', region)\
267
.replace('<SNSTopicOwnerAccountId>', account_id)\
268
.replace('<SNSTopicName>', topic)
269
270
topic_result = self.sns.create_topic(Name=topic)
271
272
try:
273
# Merge any existing topic policy with our new policy statements
274
topic_attr = self.sns.get_topic_attributes(
275
TopicArn=topic_result['TopicArn'])
276
277
policy = self.merge_sns_policy(topic_attr['Attributes']['Policy'],
278
policy)
279
280
LOG.debug('Topic policy:\n{0}'.format(policy))
281
282
# Set the topic policy
283
self.sns.set_topic_attributes(TopicArn=topic_result['TopicArn'],
284
AttributeName='Policy',
285
AttributeValue=policy)
286
except Exception:
287
# Roll back topic creation
288
self.sns.delete_topic(TopicArn=topic_result['TopicArn'])
289
raise
290
291
return topic_result
292
293
def merge_sns_policy(self, left, right):
294
"""
295
Merge two SNS topic policy documents. The id information from
296
``left`` is used in the final document, and the statements
297
from ``right`` are merged into ``left``.
298
299
http://docs.aws.amazon.com/sns/latest/dg/BasicStructure.html
300
301
:type left: string
302
:param left: First policy JSON document
303
:type right: string
304
:param right: Second policy JSON document
305
:rtype: string
306
:return: Merged policy JSON
307
"""
308
left_parsed = json.loads(left)
309
right_parsed = json.loads(right)
310
left_parsed['Statement'] += right_parsed['Statement']
311
return json.dumps(left_parsed)
312
313
def upsert_cloudtrail_config(self, name, bucket, prefix, topic, gse):
314
"""
315
Either create or update the CloudTrail configuration depending on
316
whether this command is a create or update command.
317
"""
318
sys.stdout.write('Creating/updating CloudTrail configuration...\n')
319
config = {
320
'Name': name
321
}
322
if bucket is not None:
323
config['S3BucketName'] = bucket
324
if prefix is not None:
325
config['S3KeyPrefix'] = prefix
326
if topic is not None:
327
config['SnsTopicName'] = topic
328
if gse is not None:
329
config['IncludeGlobalServiceEvents'] = gse
330
if not self.UPDATE:
331
self.cloudtrail.create_trail(**config)
332
else:
333
self.cloudtrail.update_trail(**config)
334
return self.cloudtrail.describe_trails()
335
336
def start_cloudtrail(self, name):
337
"""
338
Start the CloudTrail service, which begins logging.
339
"""
340
sys.stdout.write('Starting CloudTrail service...\n')
341
return self.cloudtrail.start_logging(Name=name)
342
343
344
class CloudTrailUpdate(CloudTrailSubscribe):
345
"""
346
Like subscribe above, but the update version of the command.
347
"""
348
NAME = 'update-subscription'
349
UPDATE = True
350
351
DESCRIPTION = ('Updates any of the trail configuration settings, and'
352
' creates and configures any new AWS resources specified.')
353
354
SYNOPSIS = ('aws cloudtrail update-subscription'
355
' [(--s3-use-bucket|--s3-new-bucket) bucket-name]'
356
' [--sns-new-topic topic-name]\n')
357
358