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