Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aws
GitHub Repository: aws/aws-cli
Path: blob/develop/tests/functional/cloudtrail/test_validation.py
1567 views
1
# Copyright 2015 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 gzip
14
15
from botocore.exceptions import ClientError
16
from tests.unit.customizations.cloudtrail.test_validation import \
17
create_scenario, TEST_TRAIL_ARN, START_DATE, END_DATE, VALID_TEST_KEY, \
18
DigestProvider, MockDigestProvider, TEST_ACCOUNT_ID
19
from awscli.testutils import mock, BaseAWSCommandParamsTest
20
from awscli.customizations.cloudtrail.validation import DigestTraverser, \
21
DATE_FORMAT, format_display_date, S3ClientProvider
22
from awscli.compat import BytesIO
23
from botocore.handlers import parse_get_bucket_location
24
25
RETRIEVER_FUNCTION = 'awscli.customizations.cloudtrail.validation.create_digest_traverser'
26
START_TIME_ARG = START_DATE.strftime(DATE_FORMAT)
27
END_TIME_ARG = END_DATE.strftime(DATE_FORMAT)
28
29
30
def _gz_compress(data):
31
out = BytesIO()
32
f = gzip.GzipFile(fileobj=out, mode="wb")
33
f.write(data.encode())
34
f.close()
35
return out.getvalue()
36
37
38
def _setup_mock_traverser(mock_create_digest_traverser, key_provider,
39
digest_provider, validator):
40
def mock_create(trail_arn, cloudtrail_client, s3_client_provider,
41
organization_client, trail_source_region,
42
bucket, prefix, on_missing, on_invalid, on_gap,
43
account_id):
44
bucket = bucket or '1'
45
return DigestTraverser(
46
digest_provider=digest_provider, starting_bucket=bucket,
47
starting_prefix=prefix, public_key_provider=key_provider,
48
digest_validator=validator, on_invalid=on_invalid, on_gap=on_gap,
49
on_missing=on_missing)
50
51
mock_create_digest_traverser.side_effect = mock_create
52
53
54
class BaseCloudTrailCommandTest(BaseAWSCommandParamsTest):
55
def setUp(self):
56
super(BaseCloudTrailCommandTest, self).setUp()
57
# We need to remove this handler to ensure that we can mock out the
58
# get_bucket_location operation.
59
self.driver.session.unregister('after-call.s3.GetBucketLocation',
60
parse_get_bucket_location)
61
self._logs = [
62
{'hashValue': '44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a',
63
'oldestEventTime': '2015-08-16T22:36:54Z',
64
's3Object': 'key1',
65
'hashAlgorithm': 'SHA-256',
66
's3Bucket': '1',
67
'newestEventTime': '2015-08-16T22:36:54Z',
68
'_raw_value': '{}'},
69
{'hashValue': '7a38bf81f383f69433ad6e900d35b3e2385593f76a7b7ab5d4355b8ba41ee24b',
70
'oldestEventTime': '2015-08-16T22:54:56Z',
71
's3Object': 'key2',
72
'hashAlgorithm': 'SHA-256',
73
's3Bucket': '1',
74
'newestEventTime': '2015-08-16T22:55:49Z',
75
'_raw_value': '{"foo":"bar"}'},
76
{'hashValue': '5b1070294963f40cb5b3c7a05d3fbaf7ffe4e5d226632026e39cfeb32d349c0c',
77
'oldestEventTime': '2015-08-16T21:54:59Z',
78
's3Object': 'key3',
79
'hashAlgorithm': 'SHA-256',
80
's3Bucket': '1',
81
'newestEventTime': '2015-08-16T21:54:59Z',
82
'_raw_value': '{"baz":"qux"}'}
83
]
84
85
86
class TestCloudTrailCommand(BaseCloudTrailCommandTest):
87
def setUp(self):
88
super(TestCloudTrailCommand, self).setUp()
89
self._traverser_patch = mock.patch(RETRIEVER_FUNCTION)
90
self._mock_traverser = self._traverser_patch.start()
91
92
def tearDown(self):
93
super(TestCloudTrailCommand, self).tearDown()
94
self._traverser_patch.stop()
95
96
def test_verbose_output_shows_happy_case(self):
97
self.parsed_responses = [
98
{'LocationConstraint': 'us-east-1'},
99
{'Body': BytesIO(_gz_compress(self._logs[0]['_raw_value']))}
100
]
101
key_provider, digest_provider, validator = create_scenario(
102
['gap', 'link'], [[], [self._logs[0]]])
103
_setup_mock_traverser(self._mock_traverser, key_provider,
104
digest_provider, validator)
105
stdout, stderr, rc = self.run_cmd(
106
("cloudtrail validate-logs --trail-arn %s --start-time %s "
107
"--region us-east-1 --verbose")
108
% (TEST_TRAIL_ARN, START_TIME_ARG), 0)
109
self.assertIn('Digest file\ts3://1/%s\tvalid'
110
% digest_provider.digests[0], stdout)
111
112
def test_verbose_output_shows_valid_digests(self):
113
key_provider, digest_provider, validator = create_scenario(
114
['gap'], [])
115
_setup_mock_traverser(self._mock_traverser, key_provider,
116
digest_provider, validator)
117
stdout, stderr, rc = self.run_cmd(
118
"cloudtrail validate-logs --trail-arn %s --start-time %s --verbose"
119
% (TEST_TRAIL_ARN, START_TIME_ARG), 0)
120
self.assertIn('Digest file\ts3://1/%s\tvalid'
121
% digest_provider.digests[0], stdout)
122
123
def test_warns_when_digest_deleted(self):
124
key_provider, digest_provider, validator = create_scenario(
125
['gap', 'missing', 'link', 'missing'], [])
126
_setup_mock_traverser(self._mock_traverser, key_provider,
127
digest_provider, validator)
128
stdout, stderr, rc = self.run_cmd(
129
"cloudtrail validate-logs --trail-arn %s --start-time %s --verbose"
130
% (TEST_TRAIL_ARN, START_TIME_ARG), 1)
131
self.assertIn('Digest file\ts3://1/%s\tINVALID: not found'
132
% digest_provider.digests[1], stderr)
133
self.assertIn('Digest file\ts3://1/%s\tINVALID: not found'
134
% digest_provider.digests[3], stderr)
135
136
def test_warns_when_no_digests_in_gap(self):
137
key_provider, digest_provider, validator = create_scenario(
138
['gap', 'gap'], [])
139
_setup_mock_traverser(self._mock_traverser, key_provider,
140
digest_provider, validator)
141
stdout, stderr, rc = self.run_cmd(
142
"cloudtrail validate-logs --trail-arn %s --start-time '%s'"
143
% (TEST_TRAIL_ARN, START_TIME_ARG), 0)
144
self.assertIn(('No log files were delivered by CloudTrail between '
145
'2014-08-10T00:00:00Z and 2014-08-10T01:00:00Z'), stderr)
146
147
def test_warns_when_digest_invalid(self):
148
key_provider, digest_provider, validator = create_scenario(
149
['gap', 'invalid', 'link'], [])
150
_setup_mock_traverser(self._mock_traverser, key_provider,
151
digest_provider, validator)
152
stdout, stderr, rc = self.run_cmd(
153
"cloudtrail validate-logs --trail-arn %s --start-time %s"
154
% (TEST_TRAIL_ARN, START_TIME_ARG), 1)
155
self.assertIn('invalid error', stderr)
156
self.assertIn(
157
'Results requested for %s to ' % format_display_date(START_DATE),
158
stdout)
159
self.assertIn('2/3 digest files valid, 1/3 digest files INVALID',
160
stdout)
161
162
def test_shows_successful_summary(self):
163
key_provider, digest_provider, validator = create_scenario(
164
['gap', 'link'], [])
165
_setup_mock_traverser(self._mock_traverser, key_provider,
166
digest_provider, validator)
167
stdout, stderr, rc = self.run_cmd(
168
("cloudtrail validate-logs --trail-arn %s --start-time %s "
169
"--end-time %s --verbose")
170
% (TEST_TRAIL_ARN, START_TIME_ARG, END_TIME_ARG), 0)
171
self.assertIn(('Results requested for 2014-08-10T00:00:00Z to '
172
'2015-08-10T00:00:00Z'), stdout)
173
self.assertIn('2/2 digest files valid', stdout)
174
self.assertIn(
175
'Results found for 2014-08-10T01:00:00Z to 2014-08-10T02:30:00Z',
176
stdout)
177
178
def test_warns_when_no_digests_after_start_date(self):
179
key_provider = mock.Mock()
180
key_provider.get_public_keys.return_value = [{'Fingerprint': 'a'}]
181
digest_provider = mock.Mock()
182
digest_provider.load_digest_keys_in_range.return_value = []
183
validator = mock.Mock()
184
_setup_mock_traverser(self._mock_traverser, key_provider,
185
digest_provider, validator)
186
stdout, stderr, rc = self.run_cmd(
187
('cloudtrail validate-logs --trail-arn %s --start-time %s '
188
'--end-time %s') % (TEST_TRAIL_ARN, START_TIME_ARG, END_TIME_ARG),
189
0)
190
self.assertIn('Results requested for %s to %s\nNo digests found'
191
% (format_display_date(START_DATE),
192
format_display_date(END_DATE)), stdout)
193
194
def test_warns_when_no_digests_found_in_range(self):
195
key_provider = mock.Mock()
196
key_provider.get_public_keys.return_value = [{'Fingerprint': 'a'}]
197
digest_provider = mock.Mock()
198
digest_provider.load_digest_keys_in_range.return_value = []
199
validator = mock.Mock()
200
_setup_mock_traverser(self._mock_traverser, key_provider,
201
digest_provider, validator)
202
stdout, stderr, rc = self.run_cmd(
203
("cloudtrail validate-logs --trail-arn %s --start-time '%s' "
204
"--end-time '%s'")
205
% (TEST_TRAIL_ARN, START_TIME_ARG, END_TIME_ARG), 0)
206
self.assertIn('Results requested for %s to %s\nNo digests found'
207
% (format_display_date(START_DATE),
208
format_display_date(END_DATE)), stdout)
209
210
def test_warns_when_no_valid_digests_found_in_range(self):
211
key_provider, digest_provider, validator = create_scenario(
212
['invalid'], [])
213
_setup_mock_traverser(self._mock_traverser, key_provider,
214
digest_provider, validator)
215
stdout, stderr, rc = self.run_cmd(
216
("cloudtrail validate-logs --trail-arn %s --start-time '%s' "
217
"--end-time '%s'")
218
% (TEST_TRAIL_ARN, START_TIME_ARG, END_TIME_ARG), 1)
219
self.assertIn(
220
'Results requested for %s to %s\nNo valid digests found in range'
221
% (format_display_date(START_DATE),
222
format_display_date(END_DATE)), stdout)
223
224
def test_fails_and_warns_when_log_hash_is_invalid(self):
225
key_provider, digest_provider, validator = create_scenario(
226
['gap'], [[self._logs[0]]])
227
self.parsed_responses = [
228
{'LocationConstraint': ''},
229
{'Body': BytesIO(_gz_compress('does not match'))}
230
]
231
_setup_mock_traverser(self._mock_traverser, key_provider,
232
digest_provider, validator)
233
stdout, stderr, rc = self.run_cmd(
234
("cloudtrail validate-logs --trail-arn %s --start-time "
235
"--region us-east-1 '%s'") % (TEST_TRAIL_ARN, START_TIME_ARG), 1)
236
self.assertIn(
237
'Log file\ts3://1/key1\tINVALID: hash value doesn\'t match', stderr)
238
239
def test_validates_valid_log_files(self):
240
key_provider, digest_provider, validator = create_scenario(
241
['gap', 'link', 'link'],
242
[[self._logs[2]], [], [self._logs[0], self._logs[1]]])
243
self.parsed_responses = [
244
{'LocationConstraint': ''},
245
{'Body': BytesIO(_gz_compress(self._logs[0]['_raw_value']))},
246
{'Body': BytesIO(_gz_compress(self._logs[1]['_raw_value']))},
247
{'Body': BytesIO(_gz_compress(self._logs[2]['_raw_value']))},
248
]
249
_setup_mock_traverser(self._mock_traverser, key_provider,
250
digest_provider, validator)
251
stdout, stderr, rc = self.run_cmd(
252
"cloudtrail validate-logs --trail-arn %s --start-time %s --verbose"
253
% (TEST_TRAIL_ARN, START_TIME_ARG), 0)
254
self.assertIn('s3://1/key1', stdout)
255
self.assertIn('s3://1/key2', stdout)
256
self.assertIn('s3://1/key3', stdout)
257
258
def test_ensures_start_time_before_end_time(self):
259
stdout, stderr, rc = self.run_cmd(
260
("cloudtrail validate-logs --trail-arn %s --start-time 2015-01-01 "
261
"--end-time 2014-01-01"), 255)
262
self.assertIn('start-time must occur before end-time', stderr)
263
264
def test_fails_when_digest_not_from_same_location_as_json_contents(self):
265
key_name = END_TIME_ARG + '.json.gz'
266
digest = {'digestPublicKeyFingerprint': 'a',
267
'digestS3Bucket': 'not_same',
268
'digestS3Object': key_name,
269
'previousDigestSignature': '...',
270
'digestStartTime': '...',
271
'digestEndTime': '...'}
272
digest_provider = mock.Mock()
273
digest_provider.load_digest_keys_in_range.return_value = [key_name]
274
digest_provider.fetch_digest.return_value = (digest, key_name)
275
_setup_mock_traverser(self._mock_traverser, mock.Mock(),
276
digest_provider, mock.Mock())
277
stdout, stderr, rc = self.run_cmd(
278
"cloudtrail validate-logs --trail-arn %s --start-time %s"
279
% (TEST_TRAIL_ARN, START_TIME_ARG), 1)
280
self.assertIn(
281
('Digest file\ts3://1/%s\tINVALID: has been moved from its '
282
'original location' % key_name), stderr)
283
284
def test_fails_when_digest_is_missing_keys_before_validation(self):
285
digest = {}
286
digest_provider = mock.Mock()
287
key_name = END_TIME_ARG + '.json.gz'
288
digest_provider.load_digest_keys_in_range.return_value = [key_name]
289
digest_provider.fetch_digest.return_value = (digest, key_name)
290
_setup_mock_traverser(self._mock_traverser, mock.Mock(),
291
digest_provider, mock.Mock())
292
stdout, stderr, rc = self.run_cmd(
293
"cloudtrail validate-logs --trail-arn %s --start-time %s"
294
% (TEST_TRAIL_ARN, START_TIME_ARG), 1)
295
self.assertIn(
296
'Digest file\ts3://1/%s\tINVALID: invalid format' % key_name,
297
stderr)
298
299
def test_fails_when_digest_metadata_is_missing(self):
300
key = MockDigestProvider([]).get_key_at_position(1)
301
self.parsed_responses = [
302
{'LocationConstraint': ''},
303
{'Contents': [{'Key': key}]},
304
{'Body': BytesIO(_gz_compress(self._logs[0]['_raw_value'])),
305
'Metadata': {}},
306
]
307
s3_client_provider = S3ClientProvider(self.driver.session, 'us-east-1')
308
digest_provider = DigestProvider(
309
s3_client_provider, TEST_ACCOUNT_ID, 'foo', 'us-east-1')
310
key_provider = mock.Mock()
311
key_provider.get_public_keys.return_value = {
312
'a': {'Value': VALID_TEST_KEY}
313
}
314
_setup_mock_traverser(self._mock_traverser, key_provider,
315
digest_provider, mock.Mock())
316
stdout, stderr, rc = self.run_cmd(
317
("cloudtrail validate-logs --trail-arn %s --start-time %s "
318
"--region us-east-1") % (TEST_TRAIL_ARN, START_TIME_ARG), 1)
319
self.assertIn(
320
'Digest file\ts3://1/%s\tINVALID: signature verification failed'
321
% key, stderr)
322
323
def test_follows_trails_when_bucket_changes(self):
324
self.parsed_responses = [
325
{'LocationConstraint': 'us-east-1'},
326
{'Body': BytesIO(_gz_compress(self._logs[0]['_raw_value']))},
327
{'LocationConstraint': 'us-west-2'},
328
{'LocationConstraint': 'eu-west-1'}
329
]
330
key_provider, digest_provider, validator = create_scenario(
331
['gap', 'bucket_change', 'link', 'bucket_change', 'link'],
332
[[], [self._logs[0]], [], [], []])
333
_setup_mock_traverser(self._mock_traverser, key_provider,
334
digest_provider, validator)
335
stdout, stderr, rc = self.run_cmd(
336
("cloudtrail validate-logs --trail-arn %s --start-time %s "
337
"--region us-east-1 --verbose")
338
% (TEST_TRAIL_ARN, START_TIME_ARG), 0)
339
self.assertIn('Digest file\ts3://3/%s\tvalid'
340
% digest_provider.digests[0], stdout)
341
self.assertIn('Digest file\ts3://2/%s\tvalid'
342
% digest_provider.digests[1], stdout)
343
self.assertIn('Digest file\ts3://2/%s\tvalid'
344
% digest_provider.digests[2], stdout)
345
self.assertIn('Digest file\ts3://1/%s\tvalid'
346
% digest_provider.digests[3], stdout)
347
self.assertIn('Digest file\ts3://1/%s\tvalid'
348
% digest_provider.digests[4], stdout)
349
350
351
class TestCloudTrailCommandWithMissingLogs(BaseCloudTrailCommandTest):
352
"""This test class is necessary in order to override the default patching
353
behavior of BaseAWSCommandParamsTest. Instead of returning responses from
354
a queue, we want to raise a ClientError.
355
"""
356
def test_fails_and_warns_when_log_is_deleted(self):
357
# Override the default request patching because we need to
358
# raise a ClientError exception.
359
key_provider, digest_provider, validator = create_scenario(
360
['gap'], [[self._logs[0]]])
361
with mock.patch(RETRIEVER_FUNCTION) as mock_create_digest_traverser:
362
_setup_mock_traverser(mock_create_digest_traverser,
363
key_provider, digest_provider, validator)
364
stdout, stderr, rc = self.run_cmd(
365
"cloudtrail validate-logs --trail-arn %s --start-time '%s'"
366
% (TEST_TRAIL_ARN, START_TIME_ARG), 1)
367
self.assertIn(
368
'Log file\ts3://1/key1\tINVALID: not found\n\n', stderr)
369
370
def patch_make_request(self):
371
"""Override the default request patching because we need to
372
raise a ClientError exception.
373
"""
374
self.make_request_is_patched = True
375
make_request_patch = self.make_request_patch.start()
376
make_request_patch.side_effect = ClientError(
377
{'Error': {'Code': 'NoSuchKey', 'Message': 'foo'}},
378
'GetObject')
379
380