Esempio n. 1
0
 def __init__(self,
              region="us-east-1",
              iam_role=None,
              parameters=None,
              awsscripter_user_data=None,
              hooks=None,
              s3_details=None,
              dependencies=None,
              role_arn=None,
              protected=False,
              tags=None,
              notifications=None,
              on_failure=None):
     #self.logger = logging.getLogger(__name__)
     self.connection_manager = ConnectionManager(region, iam_role)
     self.hooks = hooks or {}
     self.parameters = parameters or {}
     self.awsscripter_user_data = awsscripter_user_data or {}
     self.notifications = notifications or []
     self.s3_details = s3_details
     self.protected = protected
     self.role_arn = role_arn
     self.on_failure = on_failure
     self.dependencies = dependencies or []
     self.tags = tags or {}
Esempio n. 2
0
    def setup_method(self, test_method):
        self.iam_role = None
        self.profile = None
        self.region = "eu-west-1"

        self.connection_manager = ConnectionManager(region=self.region,
                                                    iam_role=self.iam_role,
                                                    profile=self.profile)
Esempio n. 3
0
    def test_connection_manager_initialised_with_no_optional_parameters(self):
        connection_manager = ConnectionManager(region=sentinel.region)

        assert connection_manager.iam_role is None
        assert connection_manager.profile is None
        assert connection_manager.region == sentinel.region
        assert connection_manager._boto_session is None
        assert connection_manager.clients == {}
Esempio n. 4
0
 def test_connection_manager_initialised_with_all_parameters(self):
     connection_manager = ConnectionManager(region=self.region,
                                            iam_role="role",
                                            profile="profile")
     assert connection_manager.iam_role == "role"
     assert connection_manager.profile == "profile"
     assert connection_manager.region == self.region
     assert connection_manager._boto_session is None
     assert connection_manager.clients == {}
Esempio n. 5
0
    def __init__(self,
                 name,
                 project_code,
                 template_path,
                 region,
                 iam_role=None,
                 parameters=None,
                 awsscripter_user_data=None,
                 hooks=None,
                 s3_details=None,
                 dependencies=None,
                 role_arn=None,
                 protected=False,
                 tags=None,
                 external_name=None,
                 notifications=None,
                 on_failure=None):
        self.logger = logging.getLogger(__name__)

        self.name = name
        self.project_code = project_code

        self.external_name = external_name or \
            get_external_stack_name(self.project_code, self.name)

        self.connection_manager = ConnectionManager(region, iam_role)

        self.hooks = hooks or {}
        self.parameters = parameters or {}
        self.awsscripter_user_data = awsscripter_user_data or {}
        self.notifications = notifications or []

        self.template_path = template_path
        self.s3_details = s3_details
        self._template = None

        self.protected = protected
        self.role_arn = role_arn
        self.on_failure = on_failure
        self.dependencies = dependencies or []
        self.tags = tags or {}
Esempio n. 6
0
def list_ec2():
    print ("listing all ec2 machines in all regions")
    connection_manager = ConnectionManager('us-east-1', iam_role=None)
    list_ec2_kwargs = {
    }
    response = connection_manager.call(
        service="ec2",
        command="describe_regions",
        kwargs=list_ec2_kwargs
    )
    logger = logging.getLogger(__name__)
    logger.info(
        "Generated credential report response: %s", response['Regions']
    )
    ec2_regions = [region['RegionName'] for region in response['Regions']]
    for region in ec2_regions:
        connection_manager = ConnectionManager('us-east-1', iam_role=None)
        instances = connection_manager.call(
        service="ec2",
        command="describe_instances",
        kwargs=list_ec2_kwargs
        )
        for instance in instances:
            if instance.state["Name"] == "running":
                print(instance.id, instance.instance_type, region)
Esempio n. 7
0
class Control():
    # --- Script controls ---

    # CIS Benchmark version referenced. Only used in web report.
    AWS_CIS_BENCHMARK_VERSION = "1.1"

    # Would you like a HTML file generated with the result?
    # This file will be delivered using a signed URL.
    S3_WEB_REPORT = True

    # Where should the report be delivered to?
    # Make sure to update permissions for the Lambda role if you change bucket name.
    S3_WEB_REPORT_BUCKET = "CHANGE_ME_TO_YOUR_S3_BUCKET"

    # Create separate report files?
    # This will add date and account number as prefix. Example: cis_report_111111111111_161220_1213.html
    S3_WEB_REPORT_NAME_DETAILS = True

    # How many hours should the report be available? Default = 168h/7days
    S3_WEB_REPORT_EXPIRE = "168"

    # Set to true if you wish to anonymize the account number in the report.
    # This is mostly used for demo/sharing purposes.
    S3_WEB_REPORT_OBFUSCATE_ACCOUNT = False

    # Would  you like to send the report signedURL to an SNS topic
    SEND_REPORT_URL_TO_SNS = False
    SNS_TOPIC_ARN = "CHANGE_ME_TO_YOUR_TOPIC_ARN"

    # Would you like to print the results as JSON to output?
    SCRIPT_OUTPUT_JSON = True

    # Would you like to supress all output except JSON result?
    # Can be used when you want to pipe result to another system.
    # If using S3 reporting, please enable SNS integration to get S3 signed URL
    OUTPUT_ONLY_JSON = False

    # Control 1.1 - Days allowed since use of root account.
    CONTROL_1_1_DAYS = 0
    """def __init__(self,parameter=None):
        self.connection_manager = ConnectionManager(region="us-east-1")
        self.parameter = "list_virtual_mfa_devices
    """ ""

    def __init__(self,
                 region="us-east-1",
                 iam_role=None,
                 parameters=None,
                 awsscripter_user_data=None,
                 hooks=None,
                 s3_details=None,
                 dependencies=None,
                 role_arn=None,
                 protected=False,
                 tags=None,
                 notifications=None,
                 on_failure=None):
        #self.logger = logging.getLogger(__name__)
        self.connection_manager = ConnectionManager(region, iam_role)
        self.hooks = hooks or {}
        self.parameters = parameters or {}
        self.awsscripter_user_data = awsscripter_user_data or {}
        self.notifications = notifications or []
        self.s3_details = s3_details
        self.protected = protected
        self.role_arn = role_arn
        self.on_failure = on_failure
        self.dependencies = dependencies or []
        self.tags = tags or {}

    def _format_parameters(self, parameters):
        """
        Converts CloudFormation parameters to the format used by Boto3.

        :param parameters: A dictionary of parameters.
        :type parameters: dict
        :returns: A list of the formatted parameters.
        :rtype: list
        """
        formatted_parameters = []
        for name, value in parameters.items():
            if value is None:
                continue
            if isinstance(value, list):
                value = ",".join(value)
            formatted_parameters.append({
                "ParameterKey": name,
                "ParameterValue": value
            })

        return formatted_parameters

    # --- 1 Identity and Access Management ---

    # 1.1 Avoid the use of the "root" account (Scored)
    def control_1_1_root_use(self, credreport):
        """Summary

        Args:
            credreport (TYPE): Description

        Returns:
            TYPE: Description
        """
        result = True
        failReason = ""
        offenders = []
        control = "1.1"
        description = "Avoid the use of the root account"
        scored = True
        if "Fail" in credreport:  # Report failure in control
            sys.exit(credreport)
        # Check if root is used in the last 24h
        now = time.strftime('%Y-%m-%dT%H:%M:%S+00:00',
                            time.gmtime(time.time()))
        frm = "%Y-%m-%dT%H:%M:%S+00:00"

        try:
            pwdDelta = (
                datetime.strptime(now, frm) -
                datetime.strptime(credreport[0]['password_last_used'], frm))
            if (pwdDelta.days == self.CONTROL_1_1_DAYS) & (
                    pwdDelta.seconds > 0):  # Used within last 24h
                failReason = "Used within 24h"
                result = False
        except:
            if credreport[0]['password_last_used'] == "N/A" or "no_information":
                pass
            else:
                print("Something went wrong")

        try:
            key1Delta = (datetime.strptime(now, frm) - datetime.strptime(
                credreport[0]['access_key_1_last_used_date'], frm))
            if (key1Delta.days == self.CONTROL_1_1_DAYS) & (
                    key1Delta.seconds > 0):  # Used within last 24h
                failReason = "Used within 24h"
                result = False
        except:
            if credreport[0][
                    'access_key_1_last_used_date'] == "N/A" or "no_information":
                pass
            else:
                print("Something went wrong")
        try:
            key2Delta = datetime.strptime(now, frm) - datetime.strptime(
                credreport[0]['access_key_2_last_used_date'], frm)
            if (key2Delta.days == self.CONTROL_1_1_DAYS) & (
                    key2Delta.seconds > 0):  # Used within last 24h
                failReason = "Used within 24h"
                result = False
        except:
            if credreport[0][
                    'access_key_2_last_used_date'] == "N/A" or "no_information":
                pass
            else:
                print("Something went wrong")
        return {
            'Result': result,
            'failReason': failReason,
            'Offenders': offenders,
            'ScoredControl': scored,
            'Description': description,
            'ControlId': control
        }

    def control_1_2_root_mfa_enabled(self):
        """Summary

        Returns:
            TYPE: Description
        """
        result = True
        failReason = ""
        offenders = []
        control = "1.2"
        description = "Ensure MFA is enabled for the root account"
        scored = True
        response = self.connection_manager.call(
            service='iam', command='get_account_summary',
            kwargs=None)  #Audit.IAM_CLIENT.get_account_summary()
        if response['SummaryMap']['AccountMFAEnabled'] != 1:
            result = False
            failReason = "Root account not using MFA"
        return {
            'Result': result,
            'failReason': failReason,
            'Offenders': offenders,
            'ScoredControl': scored,
            'Description': description,
            'ControlId': control
        }

    def control_1_3_no_active_root_accesskey_used(self, credreport):
        """Summary

        Returns:
            TYPE: Description
        """
        result = True
        failReason = ""
        offenders = []
        control = "1.3"
        description = "No Root access key should be used"
        scored = False
        offenders = []
        for n, _ in enumerate(credreport):
            if (credreport[n]['access_key_1_active']
                    or credreport[n]['access_key_2_active'] == 'true'):
                result = False
            else:
                offenders = "root"
                failReason = "Root Access Key in use"
        return {
            'Result': result,
            'failReason': failReason,
            'Offenders': offenders,
            'ScoredControl': scored,
            'Description': description,
            'ControlId': control
        }

    def control_1_4_iam_policy_no_full_star(self):
        result = True
        failReason = ""
        offenders = []
        control = "1.4"
        description = "No Full star to be used"
        scored = False
        offenders = []

        iam = boto3.client("iam")
        response = iam.list_policies(Scope='Local', OnlyAttached=True)

        for configuration_item in response["Policies"]:
            policy_info = iam.get_policy(PolicyArn=configuration_item["Arn"])
            if policy_info["Policy"]["IsAttachable"] == False:
                status = "NOT_APPLICABLE"
            else:
                policy_version = iam.get_policy_version(
                    PolicyArn=configuration_item["Arn"],
                    VersionId=policy_info['Policy']['DefaultVersionId'])
                # print("policy version +++++++++++++",policy_version)
                # print(policy_version)
                for statement in policy_version['PolicyVersion']['Document'][
                        'Statement']:
                    # print(statement)
                    star_statement = False
                    if type(statement['Action']) is list:
                        for action in statement['Action']:
                            if action == "*":
                                star_statement = True
                    else:  # just one Action
                        if statement['Action'] == "*":
                            star_statement = True

                    star_resource = False
                    if type(statement['Resource']) is list:
                        for action in statement['Resource']:
                            if action == "*":
                                star_resource = True
                    else:  # just one Resource
                        if statement['Resource'] == "*":
                            star_resource = True

                    if star_statement and star_resource:
                        status = 'NON_COMPLIANT'
                    else:
                        status = 'COMPLIANT'

            ResourceId = configuration_item["PolicyId"]
            ResourceType = "AWS::IAM::Policy"

        # Verify the AWS managed policy named AdminstratorAccess
        admin_response = iam.get_policy(
            PolicyArn="arn:aws:iam::aws:policy/AdministratorAccess")
        ResourceType = "AWS::IAM::ManagedPolicy"
        ResourceId = "AdministratorAccess"
        if int(admin_response["Policy"]["AttachmentCount"]) > 0:
            status = "NON_COMPLIANT"
        else:
            status = "COMPLIANT"

        if status == "NON_COMPLIANT":
            failReason = "full * (aka full permission) in an IAM Policy"
            result = False
        else:
            failReason = ""
            result = True
        return {
            'Result': result,
            'failReason': failReason,
            'Offenders': offenders,
            'ScoredControl': scored,
            'Description': description,
            'ControlId': control
        }

    def LM_2_1_cloudtrail_centralized_encrypted_lfi(self):
        # This rule verifies that a defined CloudTrail Trail send all logs to centralized S3 bucket.
        #
        # Scope
        # This rule covers one particular trail and is triggered periodically.
        #
        # Prerequisites
        # Configure the following parameters in the Config Rules configuration:
        # 1) RoleToAssume [present by default]
        # Configure the following in the code of this lambda function
        # 2) AWS_CLOUDTRAIL_NAME [Name of the Trail to look for]
        # 3) AWS_CLOUDTRAIL_S3_BUCKET_NAME [Name of the S3 bucket, ideally in the centralized Security Logging Account]
        # 4) AWS_CLOUDTRAIL_KMS_KEY_ARN [KMS CMK ARN used to encrypt CloudTrail, ideally in the centralized Security Logging Account]
        #
        # Use cases
        # The following logic is applied:
        # No Trail is configured -> NOT COMPLIANT
        # No Trail named AWS_CLOUDTRAIL_NAME value is configured -> NOT COMPLIANT
        # The Trail named AWS_CLOUDTRAIL_NAME value is inactive -> NOT COMPLIANT
        # The Trail named AWS_CLOUDTRAIL_NAME value is not including global resources -> NOT COMPLIANT
        # The Trail named AWS_CLOUDTRAIL_NAME value is not multi-region -> NOT COMPLIANT
        # The Trail named AWS_CLOUDTRAIL_NAME value has no Log File Integrity -> NOT COMPLIANT
        # The Trail named AWS_CLOUDTRAIL_NAME value is not logging all Management Events -> NOT COMPLIANT
        # The Trail named AWS_CLOUDTRAIL_NAME value is not logging all S3 Data Events -> NOT COMPLIANT
        # AWS_CLOUDTRAIL_S3_BUCKET_NAME is not defined -> NOT COMPLIANT
        # The Trail named AWS_CLOUDTRAIL_NAME value is not logging in AWS_CLOUDTRAIL_S3_BUCKET_NAME -> NOT COMPLIANT
        # AWS_CLOUDTRAIL_KMS_KEY_ARN is not defined -> NOT COMPLIANT
        # The Trail named AWS_CLOUDTRAIL_NAME value is not encrypted -> NOT COMPLIANT
        # The Trail named AWS_CLOUDTRAIL_NAME value is not encrypted using AWS_CLOUDTRAIL_KMS_KEY_ARN -> NOT COMPLIANT
        # The Trail named AWS_CLOUDTRAIL_NAME value is active, global, log file integrity, logging in AWS_CLOUDTRAIL_S3_BUCKET_NAME and encrypted with AWS_CLOUDTRAIL_KMS_KEY_ARN -> COMPLIANT
        """Summary

               Returns:
                   TYPE: Description
               """
        result = True
        failReason = ""
        offenders = []
        control = "2.1"
        description = "Cloud Trail lfi"
        scored = False
        offenders = []
        cloudtrail_client = boto3.client("cloudtrail")

        # AWS_CLOUDTRAIL_NAME = 'Security_Trail_DO-NOT-MODIFY'
        eval = {}
        eval["Configuration"] = cloudtrail_client.describe_trails(
        )['trailList']
        # print(eval)
        #No Trail is configured -> NOT COMPLIANT
        if len(eval['Configuration']) == 0:
            result = False
            failReason = "No configuration Found"
        for trail in eval['Configuration']:
            AWS_CLOUDTRAIL_NAME = trail['Name']
            correct_trail_name = trail
            correct_trail_status = cloudtrail_client.get_trail_status(
                Name=AWS_CLOUDTRAIL_NAME)
            correct_trail = cloudtrail_client.describe_trails(
                trailNameList=[AWS_CLOUDTRAIL_NAME])['trailList'][0]
            correct_trail_selector = \
            cloudtrail_client.get_event_selectors(TrailName=AWS_CLOUDTRAIL_NAME)['EventSelectors'][0]
            # print("print Correct_trail")
            # print(correct_trail_status)
            # print((correct_trail_selector))
            AWS_CLOUDTRAIL_S3_BUCKET_NAME = correct_trail['S3BucketName']
            # The Trail named AWS_CLOUDTRAIL_NAME value is inactive -> NOT COMPLIANT
            if correct_trail_status['IsLogging'] != True:
                result = False
                failReason = "The Trail named " + AWS_CLOUDTRAIL_NAME + " is not enabled."
            # The Trail named AWS_CLOUDTRAIL_NAME value is not including global resources -> NOT COMPLIANT
            elif correct_trail['IncludeGlobalServiceEvents'] != True:
                result = False
                failReason = "The Trail named " + AWS_CLOUDTRAIL_NAME + " is not logging global resources."
            # The Trail named AWS_CLOUDTRAIL_NAME value is not multi-region -> NOT COMPLIANTfp
            elif correct_trail['IsMultiRegionTrail'] != True:
                result = False
                failReason = "The Trail named " + AWS_CLOUDTRAIL_NAME + " is not logging in all regions."
            elif correct_trail['LogFileValidationEnabled'] != True:
                result = False
                failReason = "The Trail named " + AWS_CLOUDTRAIL_NAME + " has no log file integrity enabled."
            elif correct_trail_selector[
                    'ReadWriteType'] != 'All' or correct_trail_selector[
                        'IncludeManagementEvents'] != True:
                result = False
                failReason = "The Trail named " + AWS_CLOUDTRAIL_NAME + " do not log ALL Management events."
            elif True:  #len(correct_trail_selector['DataResources']) != 0:
                if len(correct_trail_selector['DataResources']) == 0:
                    # print("DataResources are empty")
                    result = False
                    failReason = "The Trail named " + AWS_CLOUDTRAIL_NAME + " do not log any Data Events."
                else:
                    if str(
                            correct_trail_selector['DataResources'][0]
                    ) != "{'Type': 'AWS::S3::Object', 'Values': ['arn:aws:s3']}":
                        result = False
                        failReason = "The Trail named " + AWS_CLOUDTRAIL_NAME + " do not log ALL S3 Data Events."
            elif correct_trail['S3BucketName'] != True:
                result = False
                failReason = "The Trail named " + AWS_CLOUDTRAIL_NAME + " is not logging in the S3 bucket."
            elif 'KmsKeyId' not in correct_trail:
                result = False
                failReason = "The Trail named " + AWS_CLOUDTRAIL_NAME + " is not encrypted."
            else:
                result = False
                failReason = "The Trail named " + AWS_CLOUDTRAIL_NAME + " is active and well defined to send logs to " + AWS_CLOUDTRAIL_S3_BUCKET_NAME + " and proper encryption."
        return {
            'Result': result,
            'failReason': failReason,
            'Offenders': offenders,
            'ScoredControl': scored,
            'Description': description,
            'ControlId': control
        }

    def LM_2_2_cloudwatch_event_bus_centralized(self):
        # This rule verifies that a defined Event Rule sends all events to a centralized Security Monitoring AWS Account.
        #
        # Scope
        # This rule covers all regions in one account from a single region and is triggered periodically.
        #
        # Prerequisites
        # Configure the following parameters in the Config Rules configuration:
        # 1) RoleToAssume [present by default]
        # Configure the following in the code of this lambda function
        # 2) AMAZON_CLOUDWATCH_EVENT_RULE_NAME [Name of the Rule to look for]
        # 3) AMAZON_CLOUDWATCH_EVENT_BUS_ACCOUNT_ID [Account ID of the centralized Security Monitoring Account, 12-digit]
        #
        # Use cases
        # The following logic is applied for each region:
        # No Event Rule is configured -> NOT COMPLIANT
        # No Event Rule named AMAZON_CLOUDWATCH_EVENT_RULE_NAME value is configured -> NOT COMPLIANT
        # The Event Rule named AMAZON_CLOUDWATCH_EVENT_RULE_NAME value is inactive -> NOT COMPLIANT
        # The Event Rule named AMAZON_CLOUDWATCH_EVENT_RULE_NAME value does not match the pattern "Send all events" -> NOT COMPLIANT
        # AMAZON_CLOUDWATCH_EVENT_BUS_ACCOUNT_ID is not a 12-digit string -> NOT COMPLIANT
        # The Event Rule named AMAZON_CLOUDWATCH_EVENT_RULE_NAME value has not exactly 1 target -> NOT COMPLIANT
        # The Event Rule named AMAZON_CLOUDWATCH_EVENT_RULE_NAME value has not for target the AMAZON_CLOUDWATCH_EVENT_BUS_ACCOUNT_ID default event bus -> NOT COMPLIANT
        # AMAZON_CLOUDWATCH_EVENT_RULE_NAME Event Rule is matching the pattern "Send all events" and send to AMAZON_CLOUDWATCH_EVENT_BUS_ACCOUNT_ID and is active -> COMPLIANT
        result = True
        failReason = ""
        offenders = []
        control = "2.2"
        description = "Cloud Trail Event Bus"
        scored = False
        offenders = []
        regions = boto3.client("ec2").describe_regions()['Regions']
        # print(regions)
        for region in regions:
            eval = {}
            # region_session = get_sts_session(event, rule_parameters["RoleToAssume"], region['RegionName'])
            # events_client = region_session.client("events")

            # eval['Configuration'] = events_client.list_rules()['Rules']
            events_client = boto3.client('events')
            eval = events_client.list_rules()
            # eval = {'Rules': [{ "Type": "AWS::S3::Object", "Values": ["arn:aws:s3:::mybucket/prefix", "arn:aws:s3:::mybucket2/prefix2"] }], 'ResponseMetadata': {'RequestId': 'ae4e863e-389e-11e8-b8a0-a37af6e52e05', 'HTTPStatusCode': 200, 'HTTPHeaders': {'x-amzn-requestid': 'ae4e863e-389e-11e8-b8a0-a37af6e52e05', 'content-type': 'application/x-amz-json-1.1', 'content-length': '12', 'date': 'Thu, 05 Apr 2018 06:58:01 GMT'}, 'RetryAttempts': 0}}
            #AMAZON_CLOUDWATCH_EVENT_RULE_NAME = eval['Name']
            # print("Marking eval")
            # print(eval)
            # print(eval['Rules'])
            if len(eval['Rules']) == 0:
                result = False
                failReason = "No Event Rule is configured in that region."
            else:
                for rule in eval['Rules']:
                    AMAZON_CLOUDWATCH_EVENT_RULE_NAME = rule['Name']
                    correct_rule = events_client.describe_rule(
                        Name=AMAZON_CLOUDWATCH_EVENT_RULE_NAME)
                    AMAZON_CLOUDWATCH_EVENT_BUS_ACCOUNT_ID = correct_rule[
                        'Arn'].split(":")[4]
                    # print(correct_rule)
                    # print(AMAZON_CLOUDWATCH_EVENT_RULE_NAME)
                    if correct_rule['State'] != 'ENABLED':
                        result = False
                        failReason = "The Event Rule name 4d " + AMAZON_CLOUDWATCH_EVENT_RULE_NAME + " is not enabled in that region."

                    elif correct_rule[
                            'EventPattern'] != '{"account":["' + correct_rule[
                                'Arn'].split(":")[4] + '"]}':
                        result = False
                        failReason = "The Event Rule named " + AMAZON_CLOUDWATCH_EVENT_RULE_NAME + " does not send all events (see EventPattern in that region."
                    else:
                        target = events_client.list_targets_by_rule(
                            Rule=AMAZON_CLOUDWATCH_EVENT_RULE_NAME)["Targets"]
                        if len(target) != 1:
                            result = False
                            failReason = "The Event Rule named " + AMAZON_CLOUDWATCH_EVENT_RULE_NAME + " have no or too many targets."
                        elif target[0]["Arn"] != "arn:aws:events:" + region[
                                'RegionName'] + ":" + AMAZON_CLOUDWATCH_EVENT_BUS_ACCOUNT_ID + ":event-bus/default":
                            result = False
                            failReason = "The target of the Event Rule named " + AMAZON_CLOUDWATCH_EVENT_RULE_NAME + " is not the Event Bus of " + AMAZON_CLOUDWATCH_EVENT_BUS_ACCOUNT_ID + "."
                        else:
                            result = False
                            failReason = "The Event Rule named " + AMAZON_CLOUDWATCH_EVENT_RULE_NAME + " is active and well defined to send all events to " + AMAZON_CLOUDWATCH_EVENT_BUS_ACCOUNT_ID + " via Event Bus."

            return {
                'Result': result,
                'failReason': failReason,
                'Offenders': offenders,
                'ScoredControl': scored,
                'Description': description,
                'ControlId': control
            }

    def IS_3_1_vpc_securitygroup_default_blocked(self):
        result = True
        failReason = ""
        control = "3.1"
        description = "vpc securitygroup"
        scored = False
        offenders = []
        regions = boto3.client("ec2").describe_regions()['Regions']
        for region in regions:
            # region_session = get_sts_session(event, rule_parameters["RoleToAssume"], region['RegionName'])
            ec2 = boto3.client("ec2")
            security_groups = ec2.describe_security_groups()
            # print(security_groups)
            for sg in security_groups[
                    'SecurityGroups']:  # parsing all because filtering by GroupName returns a ClientError when there are no VPCs in the region
                # print("sg is " + json.dumps(sg))
                if 'VpcId' in sg and sg['GroupName'] == "default":
                    eval = {}
                    eval["ComplianceResourceType"] = "AWS::EC2::SecurityGroup"
                    # print(eval)
                    eval['configuration'] = sg
                    # print(eval)
                    eval["ComplianceResourceId"] = "arn:aws:ec2:" + region[
                        'RegionName'] + ":" + event['configRuleArn'].split(
                            ":")[4] + ":security_group/" + sg['GroupId']
                    # there is no configrulearn passed in event
                    # print(eval)
                    if len(eval['configuration']['IpPermissions']):
                        result = "NON_COMPLIANT",
                        failReason = "There are permissions on the igress of this security group."
                    elif len(eval['configuration']['IpPermissionsEgress']):
                        result = "NON_COMPLIANT"
                        failReason = "There are permissions on the egress of this security group."
                else:
                    result = True
                    failReason = "This security group has no permission."
                return {
                    'Result': result,
                    'failReason': failReason,
                    'Offenders': offenders,
                    'ScoredControl': scored,
                    'Description': description,
                    'ControlId': control
                }

    # 3.2 | vpc_no_route_to_igw
    def IS_3_2_vpc_main_route_table_no_igw(self):
        result = True
        failReason = ""
        offenders = []
        control = "3.2"
        description = "VPC main route table no igw"
        scored = False
        offenders = []
        ec2_client = boto3.client("ec2")

        route_tables = ec2_client.describe_route_tables(
            Filters=[{
                "Name": "association.main",
                "Values": ["true"]
            }])['RouteTables']
        for route_table in route_tables:
            eval = {}
            eval["ComplianceResourceId"] = route_table['VpcId']

            igw_route = False
            for route in route_table['Routes']:
                if route['GatewayId'].startswith('igw-'):
                    igw_route = True

            if igw_route == False:
                result = True
            else:
                result = False
                failReason = "An IGW route is present in the Main route table of this VPC (RouteTableId: " + route_table[
                    'RouteTableId'] + ")."

            return {
                'Result': result,
                'failReason': failReason,
                'Offenders': offenders,
                'ScoredControl': scored,
                'Description': description,
                'ControlId': control
            }

    # 4.1 | kms_cmk_rotation_activated
    def DP_4_1_kms_cmk_rotation_activated(self):
        result = True
        failReason = ""
        offenders = []
        control = "4.1"
        description = "Kms_cmk rotation keys"
        scored = False
        offenders = []
        configuration_item = {}

        regions = boto3.client("ec2").describe_regions()['Regions']
        for region in regions:
            # region_session = get_sts_session(event, rule_parameters["RoleToAssume"], region['RegionName'])
            kms_client = boto3.client('kms')
            keys = kms_client.list_keys()
            if len(keys['Keys']) == 0:
                continue
            else:
                for key in keys['Keys']:
                    eval = {}
                    eval["ComplianceResourceType"] = "AWS::KMS::Key"
                    eval["ComplianceResourceId"] = key['KeyArn']
                    if kms_client.describe_key(
                            KeyId=key['KeyId']
                    )["KeyMetadata"]["KeyManager"] == "AWS":
                        continue
                    if kms_client.get_key_rotation_status(
                            KeyId=key['KeyId'])['KeyRotationEnabled'] == True:
                        result = True
                        failReason = "The yearly rotation is activated for this key."
                    else:
                        result = False
                        failReason = "The yearly rotation is not activated for this key."
                return {
                    'Result': result,
                    'failReason': failReason,
                    'Offenders': offenders,
                    'ScoredControl': scored,
                    'Description': description,
                    'ControlId': control
                }

    # | 4.2 | s3_bucket_public_read_prohibited
    def DP_4_2_s3_bucket_public_read_prohibited(self):
        result = True
        failReason = ""
        offenders = []
        control = "4.2"
        description = "No Public read access for S3 Buckets"
        scored = False
        offenders = []
        s3_client = boto3.client('s3')
        buckets = s3_client.list_buckets()
        public_access = False
        for bucket in buckets['Buckets']:
            acl_bucket = s3_client.get_bucket_acl(Bucket=bucket['Name'])
            for grantee in acl_bucket['Grants']:
                if (grantee['Permission']) == 'READ':
                    for uri in (grantee['Grantee'].keys()):
                        if uri == 'URI':
                            if (
                                (grantee['Grantee']['URI'] ==
                                 'http://acs.amazonaws.com/groups/global/AllUsers'
                                 )
                            ):  # && (grantee['Permission'] == 'Read')):# and grantee['Grantee']['Permission'] == 'FULL_CONTROL':
                                public_access = True
            if public_access == True:
                offenders.append(bucket['Name'])
                public_access = False
        if len(offenders) > 0:
            result = False
            failReason = "There S3 Buckets available with Public Read Access"
        return {
            'Result': result,
            'failReason': failReason,
            'Offenders': offenders,
            'ScoredControl': scored,
            'Description': description,
            'ControlId': control
        }

    # | 4.3 | s3_bucket_public_write_prohibited
    def DP_4_3_s3_bucket_public_write_prohibited(self):
        result = True
        failReason = ""
        offenders = []
        control = "4.3"
        description = "No Public write access for S3 Buckets"
        scored = False
        offenders = []
        s3_client = boto3.client('s3')
        buckets = s3_client.list_buckets()
        public_access = False
        for bucket in buckets['Buckets']:
            acl_bucket = s3_client.get_bucket_acl(Bucket=bucket['Name'])
            for grantee in acl_bucket['Grants']:
                if (grantee['Permission']) == 'WRITE':
                    for uri in (grantee['Grantee'].keys()):
                        if uri == 'URI':
                            if (
                                (grantee['Grantee']['URI'] ==
                                 'http://acs.amazonaws.com/groups/global/AllUsers'
                                 )
                            ):  # && (grantee['Permission'] == 'Read')):# and grantee['Grantee']['Permission'] == 'FULL_CONTROL':
                                public_access = True
            if public_access == True:
                offenders.append(bucket['Name'])
                public_access = False
        if len(offenders) > 0:
            result = False
            failReason = "There S3 Buckets available with Public Write Access"
        return {
            'Result': result,
            'failReason': failReason,
            'Offenders': offenders,
            'ScoredControl': scored,
            'Description': description,
            'ControlId': control
        }

    def DP_4_4_s3_bucket_ssl_requests_only(self):
        result = True
        failReason = ""
        offenders = []
        control = "4.4"
        description = "S3 bucket SSL requests only"
        scored = False
        offenders = []
        s3_client = boto3.client('s3')
        buckets = s3_client.list_buckets()
        public_access = False
        for bucket in buckets['Buckets']:
            try:
                bucket_policy = s3_client.get_bucket_policy(
                    Bucket=bucket['Name'])
                if bucket_policy:
                    bucket_policy_str = bucket_policy['Policy']
                    bucket_policy_dic = bucket_policy_str.replace("'", "\"")
                    d = json.loads(bucket_policy_dic)
                    for statement in d['Statement']:
                        for key in (statement.keys()):
                            if key == "Condition" and statement[
                                    'Effect'] == "Allow":
                                if statement['Condition']['Bool'][
                                        'aws:SecureTransport'] == "true":
                                    continue
                            if key == "Condition" and statement[
                                    'Effect'] == "Deny":
                                if statement['Condition']['Bool'][
                                        'aws:SecureTransport'] == "false":
                                    # print("good bucket")
                                    continue
                                # print(statement['Condition'])
            except Exception:
                # print("Bucket " + bucket['Name'] + "has no policy")
                offenders.append(bucket['Name'])
        if len(offenders) > 0:
            result = False
            failReason = "There S3 Buckets available without SSL enforcement Policy"
        return {
            'Result': result,
            'failReason': failReason,
            'Offenders': offenders,
            'ScoredControl': scored,
            'Description': description,
            'ControlId': control
        }
Esempio n. 8
0
 def setRegion(self, region, iam_role=None):
     self.connection_manager = ConnectionManager(region, iam_role)
Esempio n. 9
0
class CloudTrail():
    def __init__(self,
                 region="us-east-1",
                 iam_role=None,
                 parameters=None,
                 awsscripter_user_data=None,
                 hooks=None,
                 s3_details=None,
                 dependencies=None,
                 role_arn=None,
                 protected=False,
                 tags=None,
                 notifications=None,
                 on_failure=None):
        self.logger = logging.getLogger(__name__)
        self.connection_manager = ConnectionManager(region, iam_role)
        self.hooks = hooks or {}
        self.parameters = parameters or {}
        self.awsscripter_user_data = awsscripter_user_data or {}
        self.notifications = notifications or []
        self.s3_details = s3_details
        self.protected = protected
        self.role_arn = role_arn
        self.on_failure = on_failure
        self.dependencies = dependencies or []
        self.tags = tags or {}

    def setRegion(self, region, iam_role=None):
        self.connection_manager = ConnectionManager(region, iam_role)

    def _format_parameters(self, parameters):
        """
        Converts CloudFormation parameters to the format used by Boto3.

        :param parameters: A dictionary of parameters.
        :type parameters: dict
        :returns: A list of the formatted parameters.
        :rtype: list
        """
        formatted_parameters = []
        for name, value in parameters.items():
            if value is None:
                continue
            if isinstance(value, list):
                value = ",".join(value)
            formatted_parameters.append({
                "ParameterKey": name,
                "ParameterValue": value
            })

        return formatted_parameters

    def get_status(self):
        """
        Returns the credential report generation status.

        :returns: The stack's status.
        :rtype: awsscripter.stack.stack_status.StackStatus
        :raises: awsscripter.common.exceptions.StackDoesNotExistError
        """
        try:
            perform_audit_kwargs = {
                "Parameters":
                self._format_parameters(self.parameters),
                "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                "NotificationARNs":
                self.notifications,
                "Tags": [{
                    "Key": str(k),
                    "Value": str(v)
                } for k, v in self.tags.items()]
            }
            status = self.connection_manager.call(
                service="iam",
                command="get_account_password_policy",
                kwargs=perform_audit_kwargs)['State']
        except botocore.exceptions.ClientError as exp:
            raise exp
        return status

    def _wait_for_completion(self):
        """
        Waits for a credential report generarion operation to finish. Prints iam events
        while it waits.

        :returns: The final audit status.
        :rtype: awsscripter.audit.audit_status.AuditStatus
        """
        status = AuditStatus.IN_PROGRESS

        self.most_recent_event_datetime = (datetime.now(tzutc()) -
                                           timedelta(seconds=3))
        while status == AuditStatus.IN_PROGRESS:
            status = self._get_simplified_status(self.get_status())
            time.sleep(4)
        return status

    #@classmethod
    def get_regions(self):
        """Summary

        Returns:
        TYPE: Description
        """
        perform_audit_kwargs = None

        #client = boto3.client('ec2')
        #client = self.connection_manager._get_client('ec2')
        region_response = self.connection_manager.call(
            service="ec2",
            command="describe_regions",
            kwargs=perform_audit_kwargs)
        #region_response = client.describe_regions()
        regions = [
            region['RegionName'] for region in region_response['Regions']
        ]
        return regions

    def get_cloudtrails(self, regions):
        """Summary
        Returns:
            TYPE: Description
        """
        trails = dict()
        for region in regions:
            #print(region)
            self.setRegion(region, iam_role=None)
            cloudtrail_kwargs = None
            response = self.connection_manager.call(service="cloudtrail",
                                                    command="describe_trails",
                                                    kwargs=cloudtrail_kwargs)
            #print(response)
            temp = []
            for m in response['trailList']:
                if m['IsMultiRegionTrail'] is True:
                    if m['HomeRegion'] == region:
                        temp.append(m)
                else:
                    temp.append(m)
            if len(temp) > 0:
                trails[region] = temp
        return trails
Esempio n. 10
0
class PasswordPolicy():
    def __init__(self,
                 region="us-east-1",
                 iam_role=None,
                 parameters=None,
                 awsscripter_user_data=None,
                 hooks=None,
                 s3_details=None,
                 dependencies=None,
                 role_arn=None,
                 protected=False,
                 tags=None,
                 notifications=None,
                 on_failure=None):
        self.logger = logging.getLogger(__name__)
        self.connection_manager = ConnectionManager(region, iam_role)
        self.hooks = hooks or {}
        self.parameters = parameters or {}
        self.awsscripter_user_data = awsscripter_user_data or {}
        self.notifications = notifications or []
        self.s3_details = s3_details
        self.protected = protected
        self.role_arn = role_arn
        self.on_failure = on_failure
        self.dependencies = dependencies or []
        self.tags = tags or {}

    def __repr__(self):
        return ("awsscripter.audit.CredReport.CredReport("
                "region='{region}', "
                "iam_role='{iam_role}', parameters='{parameters}', "
                "awsscripter_user_data='{awsscripter_user_data}', "
                "hooks='{hooks}', s3_details='{s3_details}', "
                "dependencies='{dependencies}', role_arn='{role_arn}', "
                "protected='{protected}', tags='{tags}', "
                "notifications='{notifications}', on_failure='{on_failure}'"
                ")".format(region=self.connection_manager.region,
                           iam_role=self.connection_manager.iam_role,
                           parameters=self.parameters,
                           awsscripter_user_data=self.awsscripter_user_data,
                           hooks=self.hooks,
                           s3_details=self.s3_details,
                           dependencies=self.dependencies,
                           role_arn=self.role_arn,
                           protected=self.protected,
                           tags=self.tags,
                           notifications=self.notifications,
                           on_failure=self.on_failure))

    def _format_parameters(self, parameters):
        """
        Converts CloudFormation parameters to the format used by Boto3.

        :param parameters: A dictionary of parameters.
        :type parameters: dict
        :returns: A list of the formatted parameters.
        :rtype: list
        """
        formatted_parameters = []
        for name, value in parameters.items():
            if value is None:
                continue
            if isinstance(value, list):
                value = ",".join(value)
            formatted_parameters.append({
                "ParameterKey": name,
                "ParameterValue": value
            })

        return formatted_parameters

    def get_status(self):
        """
        Returns the credential report generation status.

        :returns: The stack's status.
        :rtype: awsscripter.stack.stack_status.StackStatus
        :raises: awsscripter.common.exceptions.StackDoesNotExistError
        """
        try:
            perform_audit_kwargs = {
                "Parameters":
                self._format_parameters(self.parameters),
                "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                "NotificationARNs":
                self.notifications,
                "Tags": [{
                    "Key": str(k),
                    "Value": str(v)
                } for k, v in self.tags.items()]
            }
            status = self.connection_manager.call(
                service="iam",
                command="get_account_password_policy",
                kwargs=perform_audit_kwargs)['State']
        except botocore.exceptions.ClientError as exp:
            raise exp
        return status

    def _wait_for_completion(self):
        """
        Waits for a credential report generarion operation to finish. Prints iam events
        while it waits.

        :returns: The final audit status.
        :rtype: awsscripter.audit.audit_status.AuditStatus
        """
        status = AuditStatus.IN_PROGRESS

        self.most_recent_event_datetime = (datetime.now(tzutc()) -
                                           timedelta(seconds=3))
        while status == AuditStatus.IN_PROGRESS:
            status = self._get_simplified_status(self.get_status())
            time.sleep(4)
        return status

    def get_account_password_policy(self):
        """Check if a IAM password policy exists, if not return false

        Returns:
            Account IAM password policy or Falsel
        """
        try:
            """Summary

                   Returns:
                       TYPE: Description
                   """
            perform_audit_kwargs = {
                "Parameters":
                self._format_parameters(self.parameters),
                "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                "NotificationARNs":
                self.notifications,
                "Tags": [{
                    "Key": str(k),
                    "Value": str(v)
                } for k, v in self.tags.items()]
            }
            #response = Audit.IAM_CLIENT.get_account_password_policy()
            response = self.connection_manager.call(
                service="iam",
                command="get_account_password_policy",
                kwargs=perform_audit_kwargs)
            return response['PasswordPolicy']
        except Exception as e:
            if "cannot be found" in str(e):
                return False
Esempio n. 11
0
class CpuMonitor(AwsBase):
    def __init__(self,
                 region,
                 iam_role=None,
                 parameters=None,
                 awsscripter_user_data=None,
                 hooks=None,
                 s3_details=None,
                 dependencies=None,
                 role_arn=None,
                 protected=False,
                 tags=None,
                 notifications=None,
                 on_failure=None):
        self.logger = logging.getLogger(__name__)
        self.connection_manager = ConnectionManager(region, iam_role)
        self.hooks = hooks or {}
        self.parameters = parameters or {}
        self.awsscripter_user_data = awsscripter_user_data or {}
        self.notifications = notifications or []
        self.s3_details = s3_details
        self.protected = protected
        self.role_arn = role_arn
        self.on_failure = on_failure
        self.dependencies = dependencies or []
        self.tags = tags or {}

    def __repr__(self):
        return ("awsscripter.monitoring.CpuMonitor.CpuMonitor("
                "region='{region}', "
                "iam_role='{iam_role}', parameters='{parameters}', "
                "awsscripter_user_data='{awsscripter_user_data}', "
                "hooks='{hooks}', s3_details='{s3_details}', "
                "dependencies='{dependencies}', role_arn='{role_arn}', "
                "protected='{protected}', tags='{tags}', "
                "notifications='{notifications}', on_failure='{on_failure}'"
                ")".format(region=self.connection_manager.region,
                           iam_role=self.connection_manager.iam_role,
                           parameters=self.parameters,
                           awsscripter_user_data=self.awsscripter_user_data,
                           hooks=self.hooks,
                           s3_details=self.s3_details,
                           dependencies=self.dependencies,
                           role_arn=self.role_arn,
                           protected=self.protected,
                           tags=self.tags,
                           notifications=self.notifications,
                           on_failure=self.on_failure))

    def _format_parameters(self, parameters):
        """
        Converts CloudFormation parameters to the format used by Boto3.

        :param parameters: A dictionary of parameters.
        :type parameters: dict
        :returns: A list of the formatted parameters.
        :rtype: list
        """
        formatted_parameters = []
        for name, value in parameters.items():
            if value is None:
                continue
            if isinstance(value, list):
                value = ",".join(value)
            formatted_parameters.append({
                "ParameterKey": name,
                "ParameterValue": value
            })

        return formatted_parameters

    def get_status(self):
        """
        Returns the credential report generation status.

        :returns: The stack's status.
        :rtype: awsscripter.stack.stack_status.StackStatus
        :raises: awsscripter.common.exceptions.StackDoesNotExistError
        """
        try:
            perform_audit_kwargs = {
                "Parameters":
                self._format_parameters(self.parameters),
                "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                "NotificationARNs":
                self.notifications,
                "Tags": [{
                    "Key": str(k),
                    "Value": str(v)
                } for k, v in self.tags.items()]
            }
            status = self.connection_manager.call(
                service="iam",
                command="generate_credential_report",
                kwargs=perform_audit_kwargs)['State']
        except botocore.exceptions.ClientError as exp:
            raise exp
        return status

    def _wait_for_completion(self):
        """
        Waits for a credential report generarion operation to finish. Prints iam events
        while it waits.

        :returns: The final audit status.
        :rtype: awsscripter.audit.audit_status.AuditStatus
        """
        status = AuditStatus.IN_PROGRESS

        self.most_recent_event_datetime = (datetime.now(tzutc()) -
                                           timedelta(seconds=3))
        while status == AuditStatus.IN_PROGRESS:
            status = self._get_simplified_status(self.get_status())
            time.sleep(4)
        return status

    def show_cpu_usage_result(self):
        """Summary

        Returns:
            TYPE: Description
        """
        self.generate_cred_report()
        perform_audit_kwargs = {
            "Parameters":
            self._format_parameters(self.parameters),
            "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
            "NotificationARNs":
            self.notifications,
            "Tags": [{
                "Key": str(k),
                "Value": str(v)
            } for k, v in self.tags.items()]
        }
        response = self.connection_manager.call(
            service="iam",
            command="get_credential_report",
            kwargs=perform_audit_kwargs)
        self.logger.debug("Response is ", response)
        report = []
        splitted_contents = response['Content'].splitlines()
        splitted_contents = [x.decode('UTF8') for x in splitted_contents]
        reader = csv.DictReader(splitted_contents, delimiter=',')
        for row in reader:
            report.append(row)

        # Verify if root key's never been used, if so add N/A
        try:
            if report[0]['access_key_1_last_used_date']:
                pass
        except:
            report[0]['access_key_1_last_used_date'] = "N/A"
        try:
            if report[0]['access_key_2_last_used_date']:
                pass
        except:
            report[0]['access_key_2_last_used_date'] = "N/A"
        return report

    def generate_cred_report(self):
        """Summary

        Returns:
            TYPE: Description
        """
        perform_audit_kwargs = {
            "Parameters":
            self._format_parameters(self.parameters),
            "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
            "NotificationARNs":
            self.notifications,
            "Tags": [{
                "Key": str(k),
                "Value": str(v)
            } for k, v in self.tags.items()]
        }
        if self.on_failure:
            perform_audit_kwargs.update({"OnFailure": self.on_failure})
        response = self.connection_manager.call(
            service="iam",
            command="generate_credential_report",
            kwargs=perform_audit_kwargs)
        self.logger.debug("Generated credential report response: %s", response)
        status = self._wait_for_completion()
        return status

    @staticmethod
    def _get_simplified_status(status):
        """
        Returns the simplified Stack Status.

        The simplified stack status is represented by the struct
        ``awsscripter.stack.stackStatus()`` and can take one of the following options:

        * STARTED
        * INPROGRESS
        * "COMPLETE"

        :param status: The CloudFormation stack status to simplify.
        :type status: str
        :returns: The stack's simplified status
        :rtype: awsscripter.stack.stack_status.StackStatus
        """
        if status.endswith("STARTED"):
            return AuditStatus.STARTED
        elif status.endswith("INPROGRESS"):
            return AuditStatus.IN_PROGRESS
        elif status.endswith("COMPLETE"):
            return AuditStatus.COMPLETE
        else:
            raise UnknownAuditStatusError("{0} is unknown".format(status))
Esempio n. 12
0
class Stack(object):
    """
    Stack stores information about a particular CloudFormation stack.

    It implements methods for carrying out stack-level operations, such as
    creating or deleting the stack.

    :param name: The name of the stack.
    :type project: str
    :param connection_manager: A connection manager, used to make Boto3 calls.
    :type connection_manager: awsscripter.connection_manager.ConnectionManager
    """

    parameters = ResolvableProperty("parameters")
    awsscripter_user_data = ResolvableProperty("awsscripter_user_data")
    notifications = ResolvableProperty("notifications")

    def __init__(self,
                 name,
                 project_code,
                 template_path,
                 region,
                 iam_role=None,
                 parameters=None,
                 awsscripter_user_data=None,
                 hooks=None,
                 s3_details=None,
                 dependencies=None,
                 role_arn=None,
                 protected=False,
                 tags=None,
                 external_name=None,
                 notifications=None,
                 on_failure=None):
        self.logger = logging.getLogger(__name__)

        self.name = name
        self.project_code = project_code

        self.external_name = external_name or \
            get_external_stack_name(self.project_code, self.name)

        self.connection_manager = ConnectionManager(region, iam_role)

        self.hooks = hooks or {}
        self.parameters = parameters or {}
        self.awsscripter_user_data = awsscripter_user_data or {}
        self.notifications = notifications or []

        self.template_path = template_path
        self.s3_details = s3_details
        self._template = None

        self.protected = protected
        self.role_arn = role_arn
        self.on_failure = on_failure
        self.dependencies = dependencies or []
        self.tags = tags or {}

    def __repr__(self):
        return ("awsscripter.stack.stack.Stack("
                "name='{name}', project_code='{project_code}', "
                "template_path='{template_path}', region='{region}', "
                "iam_role='{iam_role}', parameters='{parameters}', "
                "awsscripter_user_data='{awsscripter_user_data}', "
                "hooks='{hooks}', s3_details='{s3_details}', "
                "dependencies='{dependencies}', role_arn='{role_arn}', "
                "protected='{protected}', tags='{tags}', "
                "external_name='{external_name}', "
                "notifications='{notifications}', on_failure='{on_failure}'"
                ")".format(name=self.name,
                           project_code=self.project_code,
                           template_path=self.template_path,
                           region=self.connection_manager.region,
                           iam_role=self.connection_manager.iam_role,
                           parameters=self.parameters,
                           awsscripter_user_data=self.awsscripter_user_data,
                           hooks=self.hooks,
                           s3_details=self.s3_details,
                           dependencies=self.dependencies,
                           role_arn=self.role_arn,
                           protected=self.protected,
                           tags=self.tags,
                           external_name=self.external_name,
                           notifications=self.notifications,
                           on_failure=self.on_failure))

    @property
    def template(self):
        """
        Returns the CloudFormation template used to create the stack.

        :returns: The stack's template.
        :rtype: str
        """
        if self._template is None:
            self._template = Template(
                path=self.template_path,
                awsscripter_user_data=self.awsscripter_user_data,
                s3_details=self.s3_details,
                connection_manager=self.connection_manager)
        return self._template

    @add_stack_hooks
    def create(self):
        """
        Creates the stack.

        :returns: The stack's status.
        :rtype: awsscripter.stack.stack_status.StackStatus
        """
        self._protect_execution()
        self.logger.info("%s - Creating stack", self.name)
        create_stack_kwargs = {
            "StackName":
            self.external_name,
            "Parameters":
            self._format_parameters(self.parameters),
            "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
            "NotificationARNs":
            self.notifications,
            "Tags": [{
                "Key": str(k),
                "Value": str(v)
            } for k, v in self.tags.items()]
        }
        if self.on_failure:
            create_stack_kwargs.update({"OnFailure": self.on_failure})
        create_stack_kwargs.update(self.template.get_boto_call_parameter())
        create_stack_kwargs.update(self._get_role_arn())
        response = self.connection_manager.call(service="cloudformation",
                                                command="create_stack",
                                                kwargs=create_stack_kwargs)
        self.logger.debug("%s - Create stack response: %s", self.name,
                          response)

        status = self._wait_for_completion()

        return status

    @add_stack_hooks
    def update(self):
        """
        Updates the stack.

        :returns: The stack's status.
        :rtype: awsscripter.stack.stack_status.StackStatus
        """
        self._protect_execution()
        self.logger.info("%s - Updating stack", self.name)
        update_stack_kwargs = {
            "StackName":
            self.external_name,
            "Parameters":
            self._format_parameters(self.parameters),
            "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
            "NotificationARNs":
            self.notifications,
            "Tags": [{
                "Key": str(k),
                "Value": str(v)
            } for k, v in self.tags.items()]
        }
        update_stack_kwargs.update(self.template.get_boto_call_parameter())
        update_stack_kwargs.update(self._get_role_arn())
        response = self.connection_manager.call(service="cloudformation",
                                                command="update_stack",
                                                kwargs=update_stack_kwargs)
        self.logger.debug("%s - Update stack response: %s", self.name,
                          response)

        status = self._wait_for_completion()

        return status

    def launch(self):
        """
        Launches the stack.

        If the stack status is create_complete or rollback_complete, the
        stack is deleted. Launch thena tries to create or update the stack,
        depending if it already exists. If there are no updates to be
        performed, launch exits gracefully.

        :returns: The stack's status.
        :rtype: awsscripter.stack.stack_status.StackStatus
        """
        self._protect_execution()
        self.logger.info("%s - Launching stack", self.name)
        try:
            existing_status = self.get_status()
        except StackDoesNotExistError:
            existing_status = "PENDING"

        self.logger.info("%s - Stack is in the %s state", self.name,
                         existing_status)

        if existing_status == "PENDING":
            status = self.create()
        elif existing_status in ["CREATE_FAILED", "ROLLBACK_COMPLETE"]:
            self.delete()
            status = self.create()
        elif existing_status.endswith("COMPLETE"):
            try:
                status = self.update()
            except botocore.exceptions.ClientError as exp:
                error_message = exp.response["Error"]["Message"]
                if error_message == "No updates are to be performed.":
                    self.logger.info("%s - No updates to perform.", self.name)
                    status = StackStatus.COMPLETE
                else:
                    raise
            status = status
        elif existing_status.endswith("IN_PROGRESS"):
            self.logger.info(
                "%s - Stack action is already in progress state and cannot "
                "be updated", self.name)
            status = StackStatus.IN_PROGRESS
        elif existing_status.endswith("FAILED"):
            status = StackStatus.FAILED
            raise CannotUpdateFailedStackError(
                "'{0}' is in a the state '{1}' and cannot be updated".format(
                    self.name, existing_status))
        else:
            raise UnknownStackStatusError(
                "{0} is unknown".format(existing_status))
        return status

    @add_stack_hooks
    def delete(self):
        """
        Deletes the stack.

        :returns: The stack's status.
        :rtype: awsscripter.stack.stack_status.StackStatus
        """
        self._protect_execution()
        self.logger.info("%s - Deleting stack", self.name)
        try:
            status = self.get_status()
        except StackDoesNotExistError:
            self.logger.info("%s does not exist.", self.name)
            status = StackStatus.COMPLETE
            return status

        delete_stack_kwargs = {"StackName": self.external_name}
        delete_stack_kwargs.update(self._get_role_arn())
        self.connection_manager.call(service="cloudformation",
                                     command="delete_stack",
                                     kwargs=delete_stack_kwargs)

        try:
            status = self._wait_for_completion()
        except StackDoesNotExistError:
            status = StackStatus.COMPLETE
        except botocore.exceptions.ClientError as error:
            if error.response["Error"]["Message"].endswith("does not exist"):
                status = StackStatus.COMPLETE
            else:
                raise
        self.logger.info("%s - delete %s", self.name, status)
        return status

    def lock(self):
        """
        Locks the stack by applying a deny all updates stack policy.
        """
        policy_path = os.path.join(os.path.dirname(__file__),
                                   "stack_policies/lock.json")
        self.set_policy(policy_path)
        self.logger.info("%s - Successfully locked stack", self.name)

    def unlock(self):
        """
        Unlocks the stack by applying an allow all updates stack policy.
        """
        policy_path = os.path.join(os.path.dirname(__file__),
                                   "stack_policies/unlock.json")
        self.set_policy(policy_path)
        self.logger.info("%s - Successfully unlocked stack", self.name)

    def describe(self):
        """
        Returns the a description of the stack.

        :returns: A stack description.
        :rtype: dict
        """
        return self.connection_manager.call(
            service="cloudformation",
            command="describe_stacks",
            kwargs={"StackName": self.external_name})

    def describe_events(self):
        """
        Returns a dictionary contianing the stack events.

        :returns: The CloudFormation events for a stack.
        :rtype: dict
        """
        return self.connection_manager.call(
            service="cloudformation",
            command="describe_stack_events",
            kwargs={"StackName": self.external_name})

    def describe_resources(self):
        """
        Returns the logical and physical resource IDs of the stack's resources.

        :returns: Information about the stack's resources.
        :rtype: dict
        """
        self.logger.debug("%s - Describing stack resources", self.name)
        response = self.connection_manager.call(
            service="cloudformation",
            command="describe_stack_resources",
            kwargs={"StackName": self.external_name})
        self.logger.debug("%s - Describe stack resource response: %s",
                          self.name, response)

        desired_properties = ["LogicalResourceId", "PhysicalResourceId"]

        formatted_response = [{
            k: v
            for k, v in item.items() if k in desired_properties
        } for item in response["StackResources"]]
        return formatted_response

    def describe_outputs(self):
        """
        Returns a list of stack outputs.

        :returns: The stack's outputs.
        :rtype: list
        """
        self.logger.debug("%s - Describing stack outputs", self.name)
        response = self.describe()

        return response["Stacks"][0].get("Outputs", [])

    def continue_update_rollback(self):
        """
        Rolls back a stack in the UPDATE_ROLLBACK_FAILED state to
        UPDATE_ROLLBACK_COMPLETE.
        """
        self.logger.debug("%s - Continuing update rollback", self.name)
        continue_update_rollback_kwargs = {"StackName": self.external_name}
        continue_update_rollback_kwargs.update(self._get_role_arn())
        self.connection_manager.call(service="cloudformation",
                                     command="continue_update_rollback",
                                     kwargs=continue_update_rollback_kwargs)
        self.logger.info(
            "%s - Successfully initiated continuation of update rollback",
            self.name)

    def set_policy(self, policy_path):
        """
        Applies a stack policy.

        :param policy_path: the path of json file containing a aws policy
        :type policy_path: str
        """
        with open(policy_path) as f:
            policy = f.read()

        self.logger.debug("%s - Setting stack policy: \n%s", self.name, policy)
        self.connection_manager.call(service="cloudformation",
                                     command="set_stack_policy",
                                     kwargs={
                                         "StackName": self.external_name,
                                         "StackPolicyBody": policy
                                     })
        self.logger.info("%s - Successfully set stack policy", self.name)

    def get_policy(self):
        """
        Returns a stack's policy.

        :returns: The stack's stack policy.
        :rtype: str
        """
        self.logger.debug("%s - Getting stack policy", self.name)
        response = self.connection_manager.call(
            service="cloudformation",
            command="get_stack_policy",
            kwargs={"StackName": self.external_name})

        return response

    def create_change_set(self, change_set_name):
        """
        Creates a change set with the name ``change_set_name``.

        :param change_set_name: The name of the change set.
        :type change_set_name: str
        """
        create_change_set_kwargs = {
            "StackName":
            self.external_name,
            "Parameters":
            self._format_parameters(self.parameters),
            "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
            "ChangeSetName":
            change_set_name,
            "NotificationARNs":
            self.notifications,
            "Tags": [{
                "Key": str(k),
                "Value": str(v)
            } for k, v in self.tags.items()]
        }
        create_change_set_kwargs.update(
            self.template.get_boto_call_parameter())
        create_change_set_kwargs.update(self._get_role_arn())
        self.logger.debug("%s - Creating change set '%s'", self.name,
                          change_set_name)
        self.connection_manager.call(service="cloudformation",
                                     command="create_change_set",
                                     kwargs=create_change_set_kwargs)
        # After the call successfully completes, AWS CloudFormation
        # starts creating the change set.
        self.logger.info(
            "%s - Successfully initiated creation of change set '%s'",
            self.name, change_set_name)

    def delete_change_set(self, change_set_name):
        """
        Deletes the change set ``change_set_name``.

        :param change_set_name: The name of the change set.
        :type change_set_name: str
        """
        self.logger.debug("%s - Deleting change set '%s'", self.name,
                          change_set_name)
        self.connection_manager.call(service="cloudformation",
                                     command="delete_change_set",
                                     kwargs={
                                         "ChangeSetName": change_set_name,
                                         "StackName": self.external_name
                                     })
        # If the call successfully completes, AWS CloudFormation
        # successfully deleted the change set.
        self.logger.info("%s - Successfully deleted change set '%s'",
                         self.name, change_set_name)

    def describe_change_set(self, change_set_name):
        """
        Describes the change set ``change_set_name``.

        :param change_set_name: The name of the change set.
        :type change_set_name: str
        :returns: The description of the change set.
        :rtype: dict
        """
        self.logger.debug("%s - Describing change set '%s'", self.name,
                          change_set_name)
        return self.connection_manager.call(service="cloudformation",
                                            command="describe_change_set",
                                            kwargs={
                                                "ChangeSetName":
                                                change_set_name,
                                                "StackName": self.external_name
                                            })

    def execute_change_set(self, change_set_name):
        """
        Executes the change set ``change_set_name``.

        :param change_set_name: The name of the change set.
        :type change_set_name: str
        """
        self._protect_execution()
        self.logger.debug("%s - Executing change set '%s'", self.name,
                          change_set_name)
        self.connection_manager.call(service="cloudformation",
                                     command="execute_change_set",
                                     kwargs={
                                         "ChangeSetName": change_set_name,
                                         "StackName": self.external_name
                                     })

        status = self._wait_for_completion()
        return status

    def list_change_sets(self):
        """
        Lists the stack's change sets.

        :returns: The stack's change sets.
        :rtype: dict
        """
        self.logger.debug("%s - Listing change sets", self.name)
        return self.connection_manager.call(
            service="cloudformation",
            command="list_change_sets",
            kwargs={"StackName": self.external_name})

    def get_status(self):
        """
        Returns the stack's status.

        :returns: The stack's status.
        :rtype: awsscripter.stack.stack_status.StackStatus
        :raises: awsscripter.common.exceptions.StackDoesNotExistError
        """
        try:
            status = self.describe()["Stacks"][0]["StackStatus"]
        except botocore.exceptions.ClientError as exp:
            if exp.response["Error"]["Message"].endswith("does not exist"):
                raise StackDoesNotExistError(exp.response["Error"]["Message"])
            else:
                raise exp
        return status

    def _format_parameters(self, parameters):
        """
        Converts CloudFormation parameters to the format used by Boto3.

        :param parameters: A dictionary of parameters.
        :type parameters: dict
        :returns: A list of the formatted parameters.
        :rtype: list
        """
        formatted_parameters = []
        for name, value in parameters.items():
            if value is None:
                continue
            if isinstance(value, list):
                value = ",".join(value)
            formatted_parameters.append({
                "ParameterKey": name,
                "ParameterValue": value
            })

        return formatted_parameters

    def _get_role_arn(self):
        """
        Returns the role arn assumed by CloudFormation when building a stack.

        Returns an empty dict if no role is to be assumed.

        :returns: the a role arn
        :rtype: dict
        """
        if self.role_arn:
            return {"RoleARN": self.role_arn}
        else:
            return {}

    def _protect_execution(self):
        """
        Raises a ProtectedStackError if protect == True.
        This error is meant to stop the

        :raises: awsscripter.common.exceptions.ProtectedStackError
        """
        if self.protected:
            raise ProtectedStackError(
                "Cannot perform action on '{0}': stack protection is "
                "currently enabled".format(self.name))

    def _wait_for_completion(self):
        """
        Waits for a stack operation to finish. Prints CloudFormation events
        while it waits.

        :returns: The final stack status.
        :rtype: awsscripter.stack.stack_status.StackStatus
        """
        status = StackStatus.IN_PROGRESS

        self.most_recent_event_datetime = (datetime.now(tzutc()) -
                                           timedelta(seconds=3))
        while status == StackStatus.IN_PROGRESS:
            status = self._get_simplified_status(self.get_status())
            self._log_new_events()
            time.sleep(4)
        return status

    @staticmethod
    def _get_simplified_status(status):
        """
        Returns the simplified Stack Status.

        The simplified stack status is represented by the struct
        ``awsscripter.stack.stackStatus()`` and can take one of the following options:

        * complete
        * in_progress
        * failed

        :param status: The CloudFormation stack status to simplify.
        :type status: str
        :returns: The stack's simplified status
        :rtype: awsscripter.stack.stack_status.StackStatus
        """
        if status.endswith("ROLLBACK_COMPLETE"):
            return StackStatus.FAILED
        elif status.endswith("_COMPLETE"):
            return StackStatus.COMPLETE
        elif status.endswith("_IN_PROGRESS"):
            return StackStatus.IN_PROGRESS
        elif status.endswith("_FAILED"):
            return StackStatus.FAILED
        else:
            raise UnknownStackStatusError("{0} is unknown".format(status))

    def _log_new_events(self):
        """
        Log the latest stack events while the stack is being built.
        """
        events = self.describe_events()["StackEvents"]
        events.reverse()
        new_events = [
            event for event in events
            if event["Timestamp"] > self.most_recent_event_datetime
        ]
        for event in new_events:
            self.logger.info(" ".join([
                event["Timestamp"].replace(microsecond=0).isoformat(),
                self.name, event["LogicalResourceId"], event["ResourceType"],
                event["ResourceStatus"],
                event.get("ResourceStatusReason", "")
            ]))
            self.most_recent_event_datetime = event["Timestamp"]

    def wait_for_cs_completion(self, change_set_name):
        """
        Waits while the stack change set status is "pending".

        :param change_set_name: The name of the change set.
        :type change_set_name: str
        :returns: The change set's status.
        :rtype: awsscripter.stack.stack_status.StackChangeSetStatus
        """
        while True:
            status = self._get_cs_status(change_set_name)
            if status != StackChangeSetStatus.PENDING:
                break
            time.sleep(2)

        return status

    def _get_cs_status(self, change_set_name):
        """
        Returns the status of a change set.

        :param change_set_name: The name of the change set.
        :type change_set_name: str
        :returns: The change set's status.
        :rtype: awsscripter.stack.stack_status.StackChangeSetStatus
        """
        cs_description = self.describe_change_set(change_set_name)

        cs_status = cs_description["Status"]
        cs_exec_status = cs_description["ExecutionStatus"]
        possible_statuses = [
            "CREATE_PENDING", "CREATE_IN_PROGRESS", "CREATE_COMPLETE",
            "DELETE_COMPLETE", "FAILED"
        ]
        possible_execution_statuses = [
            "UNAVAILABLE", "AVAILABLE", "EXECUTE_IN_PROGRESS",
            "EXECUTE_COMPLETE", "EXECUTE_FAILED", "OBSOLETE"
        ]

        if cs_status not in possible_statuses:
            raise UnknownStackChangeSetStatusError(
                "Status {0} is unknown".format(cs_status))
        if cs_exec_status not in possible_execution_statuses:
            raise UnknownStackChangeSetStatusError(
                "ExecutionStatus {0} is unknown".format(cs_status))

        if (cs_status == "CREATE_COMPLETE" and cs_exec_status == "AVAILABLE"):
            return StackChangeSetStatus.READY
        elif (cs_status
              in ["CREATE_PENDING", "CREATE_IN_PROGRESS", "CREATE_COMPLETE"]
              and cs_exec_status in ["UNAVAILABLE", "AVAILABLE"]):
            return StackChangeSetStatus.PENDING
        elif (cs_status in ["DELETE_COMPLETE", "FAILED"] or cs_exec_status in [
                "EXECUTE_IN_PROGRESS", "EXECUTE_COMPLETE", "EXECUTE_FAILED",
                "OBSOLETE"
        ]):
            return StackChangeSetStatus.DEFUNCT
        else:  # pragma: no cover
            raise Exception("This else should not be reachable.")
Esempio n. 13
0
class TestConnectionManager(object):
    def setup_method(self, test_method):
        self.iam_role = None
        self.profile = None
        self.region = "eu-west-1"

        self.connection_manager = ConnectionManager(region=self.region,
                                                    iam_role=self.iam_role,
                                                    profile=self.profile)

    def test_connection_manager_initialised_with_all_parameters(self):
        connection_manager = ConnectionManager(region=self.region,
                                               iam_role="role",
                                               profile="profile")
        assert connection_manager.iam_role == "role"
        assert connection_manager.profile == "profile"
        assert connection_manager.region == self.region
        assert connection_manager._boto_session is None
        assert connection_manager.clients == {}

    def test_connection_manager_initialised_with_no_optional_parameters(self):
        connection_manager = ConnectionManager(region=sentinel.region)

        assert connection_manager.iam_role is None
        assert connection_manager.profile is None
        assert connection_manager.region == sentinel.region
        assert connection_manager._boto_session is None
        assert connection_manager.clients == {}

    def test_repr(self):
        self.connection_manager.iam_role = "role"
        self.connection_manager.profile = "profile"
        self.connection_manager.region = "region"
        response = self.connection_manager.__repr__()
        assert response == "awsscripter.connection_manager.ConnectionManager(" \
            "region='region', iam_role='role', profile='profile')"

    def test_boto_session_with_cache(self):
        self.connection_manager._boto_session = sentinel.boto_session
        assert self.connection_manager.boto_session == sentinel.boto_session

    @patch("awsscripter.connection_manager.boto3.session.Session")
    def test_boto_session_with_no_iam_role_and_no_profile(self, mock_Session):
        self.connection_manager._boto_session = None
        self.connection_manager.iam_role = None
        self.connection_manager.profile = None

        boto_session = self.connection_manager.boto_session
        assert boto_session.isinstance(mock_Session)
        mock_Session.assert_called_once_with(region_name="eu-west-1",
                                             profile_name=None)

    @patch("awsscripter.connection_manager.boto3.session.Session")
    def test_boto_session_with_no_iam_role_and_profile(self, mock_Session):
        self.connection_manager._boto_session = None
        self.connection_manager.iam_role = None
        self.connection_manager.profile = "profile"

        boto_session = self.connection_manager.boto_session
        assert boto_session.isinstance(mock_Session)
        mock_Session.assert_called_once_with(region_name="eu-west-1",
                                             profile_name="profile")

    @patch("awsscripter.connection_manager.boto3.session.Session")
    def test_boto_session_with_iam_role_and_no_profile(self, mock_Session):
        self.connection_manager._boto_session = None
        self.connection_manager.iam_role = "non-default"
        self.connection_manager.profile = None

        mock_credentials = {
            "Credentials": {
                "AccessKeyId": "id",
                "SecretAccessKey": "key",
                "SessionToken": "token",
                "Expiration": datetime(2020, 1, 1)
            }
        }

        mock_Session.return_value.client.return_value.\
            assume_role.return_value = mock_credentials

        boto_session = self.connection_manager.boto_session
        assert boto_session.isinstance(mock_Session)

        mock_Session.assert_any_call(profile_name=None,
                                     region_name=self.region)
        mock_Session.assert_any_call(aws_access_key_id="id",
                                     aws_secret_access_key="key",
                                     aws_session_token="token",
                                     region_name=self.region)

    @patch("awsscripter.connection_manager.boto3.session.Session")
    def test_boto_session_with_iam_role_and_profile(self, mock_Session):
        self.connection_manager._boto_session = None
        self.connection_manager.iam_role = "non-default"
        self.connection_manager.profile = "profile"

        mock_credentials = {
            "Credentials": {
                "AccessKeyId": "id",
                "SecretAccessKey": "key",
                "SessionToken": "token",
                "Expiration": datetime(2020, 1, 1)
            }
        }

        mock_Session.return_value.client.return_value. \
            assume_role.return_value = mock_credentials

        boto_session = self.connection_manager.boto_session
        assert boto_session.isinstance(mock_Session)

        mock_Session.assert_any_call(profile_name="profile",
                                     region_name=self.region)
        mock_Session.assert_any_call(aws_access_key_id="id",
                                     aws_secret_access_key="key",
                                     aws_session_token="token",
                                     region_name=self.region)

    @patch("awsscripter.connection_manager.boto3.session.Session")
    def test_two_boto_sessions(self, mock_Session):
        self.connection_manager._boto_session = None
        boto_session_1 = self.connection_manager.boto_session
        boto_session_2 = self.connection_manager.boto_session
        assert boto_session_1 == boto_session_2

    @patch(
        "awsscripter.connection_manager.boto3.session.Session.get_credentials")
    def test_get_client_with_no_pre_existing_clients(self,
                                                     mock_get_credentials):
        service = "s3"

        client = self.connection_manager._get_client(service)
        expected_client = Session().client(service)
        assert str(type(client)) == str(type(expected_client))

    @patch(
        "awsscripter.connection_manager.boto3.session.Session.get_credentials")
    def test_get_client_with_invalid_client_type(self, mock_get_credentials):
        service = "invalid_type"
        with pytest.raises(UnknownServiceError):
            self.connection_manager._get_client(service)

    @patch(
        "awsscripter.connection_manager.boto3.session.Session.get_credentials")
    def test_get_client_with_exisiting_client(self, mock_get_credentials):
        service = "cloudformation"
        client_1 = self.connection_manager._get_client(service)
        client_2 = self.connection_manager._get_client(service)
        assert client_1 == client_2

    @patch(
        "awsscripter.connection_manager.boto3.session.Session.get_credentials")
    def test_get_client_with_exisiting_client_and_iam_role_none(
            self, mock_get_credentials):
        service = "cloudformation"
        self.connection_manager._iam_role = None
        client_1 = self.connection_manager._get_client(service)
        client_2 = self.connection_manager._get_client(service)
        assert client_1 == client_2

    def test_clear_session_cache_if_expired_with_no_iam_role(self):
        self.connection_manager.iam_role = None
        self.connection_manager._boto_session_expiration = sentinel.expiration
        self.connection_manager.clients = sentinel.clients
        self.connection_manager._boto_session = sentinel.boto_session
        self.connection_manager._clear_session_cache_if_expired()
        assert self.connection_manager.clients == sentinel.clients
        assert self.connection_manager._boto_session == sentinel.boto_session

    @freeze_time("2000-01-30")
    def test_clear_session_cache_if_expired_with_future_date(self):
        self.connection_manager.iam_role = "iam_role"
        future_date = datetime(2015, 1, 30, tzinfo=tz.tzutc())
        self.connection_manager._boto_session_expiration = future_date
        self.connection_manager.clients = sentinel.clients
        self.connection_manager._boto_session = sentinel.boto_session
        self.connection_manager._clear_session_cache_if_expired()
        assert self.connection_manager.clients == sentinel.clients
        assert self.connection_manager._boto_session == sentinel.boto_session

    @freeze_time("2015-01-30")
    def test_clear_session_cache_if_expired_with_expired_date(self):
        self.connection_manager.iam_role = "iam_role"
        past_date = datetime(2000, 1, 30, tzinfo=tz.tzutc())
        self.connection_manager._boto_session_expiration = past_date

        self.connection_manager.clients = sentinel.clients
        self.connection_manager._boto_session = sentinel.boto_session

        self.connection_manager._clear_session_cache_if_expired()
        assert self.connection_manager.clients == {}
        assert self.connection_manager._boto_session is None

    @mock_s3
    def test_call_with_valid_service_and_call(self):
        service = 's3'
        command = 'list_buckets'

        return_value = self.connection_manager.call(service, command, {})
        assert return_value['ResponseMetadata']['HTTPStatusCode'] == 200