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 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_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_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 __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 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)
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 }
def setRegion(self, region, iam_role=None): self.connection_manager = ConnectionManager(region, iam_role)
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
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
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))
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.")
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