Beispiel #1
0
def athena(ctx, accounts):
    """Setup athena tables."""

    conn_details = {
        'account_number': ctx.config.get('aws', {}).get('organizations', {}).get('accountId', '123'),
        'assume_role': ctx.config.get('aws', {}).get('organizations', {}).get('roleName', '123'),
        'session_name': 'cloudtrail-anomaly'
    }

    ca = CloudAux(**conn_details)

    conn_details_athena = {
        'account_number': ctx.config['aws']['athena']['accountId'],
        'assume_role': ctx.config['aws']['athena']['roleName'],
        'session_name': 'cloudtrail-anomaly',
        'region': ctx.config.get('aws', {}).get('region', 'us-east-1')
    }
    ca_athena = CloudAux(**conn_details_athena)

    if not accounts:
        accounts = get_accounts_from_orgs(ca)
        log.info('Received {} accounts from Organizations'.format(len(accounts)))
    else:
        log.info('Received {} accounts from command line'.format(len(accounts)))

    for account in accounts:        
        file_name = create_table(ctx.config, accounts[0], ca_athena)

        if not file_name:
            log.error("Execution failed or timed out for account {}".format(account))
            continue
        else:
            log.info("Successfully created Athena table for account {}".format(account))

    log.info('Successfully created Athena tables')
Beispiel #2
0
def test_cloudaux():
    conn_one = {
        "account_number": "111111111111",
        "assume_role": "role_one",
        "region": "us-east-1",
        "session_name": "conn_one"
    }

    conn_two = {
        "account_number": "222222222222",
        "assume_role": "role_two",
        "region": "us-east-2",
        "session_name": "conn_two"
    }

    ca_one = CloudAux(**conn_one)
    ca_two = CloudAux(**conn_two)

    assert ca_one.conn_details["account_number"] == "111111111111"
    assert ca_one.conn_details["assume_role"] == "role_one"
    assert ca_one.conn_details["region"] == "us-east-1"
    assert ca_one.conn_details["session_name"] == "conn_one"

    assert ca_two.conn_details["account_number"] == "222222222222"
    assert ca_two.conn_details["assume_role"] == "role_two"
    assert ca_two.conn_details["region"] == "us-east-2"
    assert ca_two.conn_details["session_name"] == "conn_two"
Beispiel #3
0
 def set_sqs_policy(self, event, policy):
     conn_details = self.conn_details_from_event(event)
     ca = CloudAux(**conn_details)
     if policy:
         policy = json.dumps(policy)
     ca.call('sqs.client.set_queue_attributes',
             QueueUrl=self.queue_url_from_event(event),
             Attributes=dict(Policy=policy))
Beispiel #4
0
async def retrieve_org_structure(org_account_id: str,
                                 role_to_assume: str = "ConsoleMe",
                                 region: str = "us-east-1") -> Dict[str, Any]:
    """Retrieve org roots then recursively build a dict of child OUs and accounts.

    This is a slow and expensive operation.

    Args:
        org_account_id: ID for AWS account containing org(s)
        region: AWS region
    """
    conn_details = {
        "assume_role": role_to_assume,
        "account_number": org_account_id,
        "session_name": "ConsoleMeSCPSync",
        "region": region,
    }
    ca = CloudAux(**conn_details)
    roots = _list_org_roots(ca)
    org_structure = {}
    for root in roots:
        root_id = root["Id"]
        root["Children"] = _get_children_for_ou(ca, root["Id"])
        org_structure[root_id] = root
    return org_structure
Beispiel #5
0
def _list_org_roots(ca: CloudAux, **kwargs) -> List[Dict[str, Any]]:
    """Wrapper for organizations:ListRoots

    Args:
        ca: CloudAux instance
    """
    return ca.call("organizations.client.list_roots", **kwargs)
Beispiel #6
0
async def retrieve_scps_for_organization(
        org_account_id: str,
        role_to_assume: str = "ConsoleMe",
        region: str = "us-east-1") -> List[ServiceControlPolicyModel]:
    """Return a ServiceControlPolicyArrayModel containing all SCPs for an organization

    Args:
        org_account_id: ID for AWS account containing org(s)
        region: AWS region
    """
    conn_details = {
        "assume_role": role_to_assume,
        "account_number": org_account_id,
        "session_name": "ConsoleMeSCPSync",
        "region": region,
    }
    ca = CloudAux(**conn_details)
    all_scp_metadata = await sync_to_async(_list_service_control_policies)(ca)
    all_scp_objects = []
    for scp_metadata in all_scp_metadata:
        targets = await sync_to_async(_list_targets_for_policy)(
            ca, scp_metadata["Id"])
        policy = await _get_service_control_policy(ca, scp_metadata["Id"])
        target_models = [ServiceControlPolicyTargetModel(**t) for t in targets]
        scp_object = ServiceControlPolicyModel(
            targets=target_models,
            policy=ServiceControlPolicyDetailsModel(**policy),
        )
        all_scp_objects.append(scp_object.dict())
    return all_scp_objects
Beispiel #7
0
def _get_base(role, **conn):
    """
    Determine whether the boto get_role call needs to be made or if we already have all that data
    in the role object.
    :param role: dict containing (at the very least) role_name and/or arn.
    :param conn: dict containing enough information to make a connection to the desired account.
    :return: Camelized dict describing role containing all all base_fields.
    """
    base_fields = frozenset(['Arn', 'AssumeRolePolicyDocument', 'Path', 'RoleId', 'RoleName', 'CreateDate'])
    needs_base = False

    for field in base_fields:
        if field not in role:
            needs_base = True
            break

    if needs_base:
        role_name = _get_name_from_structure(role, 'RoleName')
        role = CloudAux.go('iam.client.get_role', RoleName=role_name, **conn)
        role = role['Role']

    # cast CreateDate from a datetime to something JSON serializable.
    role.update(dict(CreateDate=str(role['CreateDate'])))

    return role
Beispiel #8
0
        def decorated_function(*args, **kwargs):
            threads = []
            for account, region in product(accounts, regions):
                conn_dict = {
                    'tech': service,
                    'account_number': account,
                    'region': region,
                    'session_name': session_name,
                    'assume_role': assume_role,
                    'service_type': service_type,
                    'external_id': external_id
                }
                if conn_type == 'cloudaux':
                    kwargs['cloudaux'] = CloudAux(**conn_dict)
                elif conn_type == 'dict':
                    kwargs['conn_dict'] = conn_dict
                elif conn_type == 'boto3':
                    del conn_dict['tech']
                    kwargs['conn'] = boto3_cached_conn(service, **conn_dict)
                result = func(*args, **kwargs)
                if result:
                    threads.append(result)

            result = []
            for thread in threads:
                result.append(thread)
            return result
Beispiel #9
0
def _list_service_control_policies(ca: CloudAux, **kwargs) -> List[Dict]:
    """Return a complete list of service control policy metadata dicts from the paginated ListPolicies API call

    Args:
        ca: CloudAux instance
    """
    return ca.call("organizations.client.list_policies",
                   Filter="SERVICE_CONTROL_POLICY",
                   MaxResults=20,
                   **kwargs)
Beispiel #10
0
def _describe_account(ca: CloudAux, account_id: str, **kwargs) -> Dict[str, str]:
    """Wrapper for organizations:DescribeAccount

    Args:
        ca: CloudAux instance
        account_id: AWS account ID
    """
    result = ca.call(
        "organizations.client.describe_account", AccountId=account_id, **kwargs
    )
    return result.get("Account")
Beispiel #11
0
def _describe_ou(ca: CloudAux, ou_id: str, **kwargs) -> Dict[str, str]:
    """Wrapper for organizations:DescribeOrganizationalUnit

    Args:
        ca: CloudAux instance
        ou_id: organizational unit ID
    """
    result = ca.call("organizations.client.describe_organizational_unit",
                     OrganizationalUnitId=ou_id,
                     **kwargs)
    return result.get("OrganizationalUnit")
Beispiel #12
0
def _list_targets_for_policy(ca: CloudAux, scp_id: str,
                             **kwargs) -> List[Dict[str, str]]:
    """Return a complete list of target metadata dicts from the paginated ListTargetsForPolicy API call

    Args:
        ca: CloudAux instance
        scp_id: service control policy ID
    """
    return ca.call("organizations.client.list_targets_for_policy",
                   PolicyId=scp_id,
                   MaxResults=20,
                   **kwargs)
Beispiel #13
0
def _list_children_for_ou(ca: CloudAux, parent_id: str,
                          child_type: Literal["ACCOUNT",
                                              "ORGANIZATIONAL_UNIT"],
                          **kwargs) -> List[Dict[str, Any]]:
    """Wrapper for organizations:ListChildren

    Args:
        ca: CloudAux instance
        parent_id: ID of organization root or organizational unit
        child_type: ACCOUNT or ORGANIZATIONAL_UNIT
    """
    return ca.call("organizations.client.list_children",
                   ChildType=child_type,
                   ParentId=parent_id,
                   **kwargs)
Beispiel #14
0
def _get_base(user, **conn):
    base_fields = frozenset(['Arn', 'CreateDate', 'Path', 'UserId', 'UserName'])
    needs_base = False
    for field in base_fields:
        if field not in user:
            needs_base = True
            break

    if needs_base:
        user_name = _get_name_from_structure(user, 'UserName')
        user = CloudAux.go('iam.client.get_user', UserName=user_name, **conn)
        user = user['User']

    # cast CreateDate from a datetime to something JSON serializable.
    user.update(dict(CreateDate=str(user['CreateDate'])))
    if 'PasswordLastUsed' in user:
        user.update(dict(PasswordLastUsed=str(user['PasswordLastUsed'])))

    user['_version'] = 1
    return user
Beispiel #15
0
def rollback_role(account_number, role_name, dynamo_table, config, hooks, selection=None, commit=None):
    """
    Display the historical policy versions for a roll as a numbered list.  Restore to a specific version if selected.
    Indicate changes that will be made and then actually make them if commit is selected.

    Args:
        account_number (string)
        role_name (string)
        selection (int): which policy version in the list to rollback to
        commit (bool): actually make the change

    Returns:
        errors (list): if any
    """
    errors = []

    role_id = find_role_in_cache(dynamo_table, account_number, role_name)
    if not role_id:
        message = 'Could not find role with name {} in account {}'.format(role_name, account_number)
        errors.append(message)
        LOGGER.warning(message)
        return errors
    else:
        role = Role(get_role_data(dynamo_table, role_id))

    # no option selected, display a table of options
    if not selection:
        headers = ['Number', 'Source', 'Discovered', 'Permissions', 'Services']
        rows = []
        for index, policies_version in enumerate(role.policies):
            policy_permissions = roledata._get_permissions_in_policy(policies_version['Policy'])
            rows.append([index, policies_version['Source'], policies_version['Discovered'],
                        len(policy_permissions),
                        roledata._get_services_in_permissions(policy_permissions)])
        print tabulate(rows, headers=headers)
        return

    from cloudaux import CloudAux
    conn = config['connection_iam']
    conn['account_number'] = account_number
    ca = CloudAux(**conn)

    current_policies = get_role_inline_policies(role.as_dict(), **conn)

    if selection:
        pp = pprint.PrettyPrinter()

        print "Will restore the following policies:"
        pp.pprint(role.policies[int(selection)]['Policy'])

        print "Current policies:"
        pp.pprint(current_policies)

        current_permissions = roledata._get_permissions_in_policy(role.policies[-1]['Policy'])
        selected_permissions = roledata._get_permissions_in_policy(role.policies[int(selection)]['Policy'])
        restored_permissions = selected_permissions - current_permissions

        print "\nResore will return these permissions:"
        print '\n'.join([perm for perm in sorted(restored_permissions)])

    if not commit:
        return False

    # if we're restoring from a version with fewer policies than we have now, we need to remove them to
    # complete the restore.  To do so we'll store all the policy names we currently have and remove them
    # from the list as we update.  Any policy names left need to be manually removed
    policies_to_remove = current_policies.keys()

    for policy_name, policy in role.policies[int(selection)]['Policy'].items():
        try:
            LOGGER.info("Pushing cached policy: {} (role: {} account {})".format(
                policy_name,
                role.role_name,
                account_number))

            ca.call('iam.client.put_role_policy', RoleName=role.role_name, PolicyName=policy_name,
                    PolicyDocument=json.dumps(policy, indent=2, sort_keys=True))

        except botocore.exceptions.ClientError as e:
            message = "Unable to push policy {}.  Error: {} (role: {} account {})".format(
                policy_name,
                e.message,
                role.role_name,
                account_number)
            LOGGER.error(message)
            errors.append(message)

        else:
            # remove the policy name if it's in the list
            try:
                policies_to_remove.remove(policy_name)
            except Exception:
                pass

    if policies_to_remove:
        for policy_name in policies_to_remove:
            try:
                ca.call('iam.client.delete_role_policy', RoleName=role.role_name, PolicyName=policy_name)

            except botocore.excpetions.ClientError as e:
                message = "Unable to delete policy {}.  Error: {} (role: {} account {})".format(
                    policy_name,
                    e.message,
                    role.role_name,
                    account_number)
                LOGGER.error(message)
                errors.append(message)

    _update_role_data(role, dynamo_table, account_number, config, conn, hooks, source='Restore', add_no_repo=False)

    if not errors:
        LOGGER.info('Successfully restored selected version of role policies (role: {} account: {})'.format(
            role.role_name,
            account_number))
    return errors
Beispiel #16
0
def repo_role(account_number, role_name, dynamo_table, config, hooks, commit=False):
    """
    Calculate what repoing can be done for a role and then actually do it if commit is set
      1) Check that a role exists, it isn't being disqualified by a filter, and that is has fresh AA data
      2) Get the role's current permissions, repoable permissions, and the new policy if it will change
      3) Make the changes if commit is set
    Args:
        account_number (string)
        role_name (string)
        commit (bool)

    Returns:
        None
    """
    errors = []

    role_id = find_role_in_cache(dynamo_table, account_number, role_name)
    # only load partial data that we need to determine if we should keep going
    role_data = get_role_data(dynamo_table, role_id, fields=['DisqualifiedBy', 'AAData', 'RepoablePermissions',
                                                             'RoleName'])
    if not role_data:
        LOGGER.warn('Could not find role with name {}'.format(role_name))
        return
    else:
        role = Role(role_data)

    if len(role.disqualified_by) > 0:
        LOGGER.info('Cannot repo role {} in account {} because it is being disqualified by: {}'.format(
            role_name,
            account_number,
            role.disqualified_by))
        return

    if not role.aa_data:
        LOGGER.warning('ARN not found in Access Advisor: {}'.format(role.arn))
        return

    if not role.repoable_permissions:
        LOGGER.info('No permissions to repo for role {} in account {}'.format(role_name, account_number))
        return

    # if we've gotten to this point, load the rest of the role
    role = Role(get_role_data(dynamo_table, role_id))

    old_aa_data_services = []
    for aa_service in role.aa_data:
        if(datetime.datetime.strptime(aa_service['lastUpdated'], '%a, %d %b %Y %H:%M:%S %Z') <
           datetime.datetime.now() - datetime.timedelta(days=config['repo_requirements']['oldest_aa_data_days'])):
            old_aa_data_services.append(aa_service['serviceName'])

    if old_aa_data_services:
        LOGGER.error('AAData older than threshold for these services: {} (role: {}, account {})'.format(
            old_aa_data_services,
            role_name,
            account_number))
        return

    permissions = roledata._get_role_permissions(role)
    repoable_permissions = roledata._get_repoable_permissions(account_number, role.role_name, permissions, role.aa_data,
                                                              role.no_repo_permissions,
                                                              config['filter_config']['AgeFilter']['minimum_age'],
                                                              hooks)

    repoed_policies, deleted_policy_names = roledata._get_repoed_policy(role.policies[-1]['Policy'],
                                                                        repoable_permissions)

    policies_length = len(json.dumps(repoed_policies))

    if policies_length > MAX_AWS_POLICY_SIZE:
        error = ("Policies would exceed the AWS size limit after repo for role: {} in account {}.  "
                 "Please manually minify.".format(role_name, account_number))
        LOGGER.error(error)
        errors.append(error)
        return

    if not commit:
        for name in deleted_policy_names:
            LOGGER.info('Would delete policy from {} with name {} in account {}'.format(
                role_name,
                name,
                account_number))
        if repoed_policies:
            LOGGER.info('Would replace policies for role {} with: \n{} in account {}'.format(
                role_name,
                json.dumps(repoed_policies, indent=2, sort_keys=True),
                account_number))
        return

    conn = config['connection_iam']
    conn['account_number'] = account_number
    ca = CloudAux(**conn)

    for name in deleted_policy_names:
        LOGGER.info('Deleting policy with name {} from {} in account {}'.format(name, role.role_name, account_number))
        try:
            ca.call('iam.client.delete_role_policy', RoleName=role.role_name, PolicyName=name)
        except botocore.exceptions.ClientError as e:
            error = 'Error deleting policy: {} from role: {} in account {}.  Exception: {}'.format(
                name,
                role.role_name,
                account_number,
                e)
            LOGGER.error(error)
            errors.append(error)

    if repoed_policies:
        LOGGER.info('Replacing Policies With: \n{} (role: {} account: {})'.format(
            json.dumps(repoed_policies, indent=2, sort_keys=True),
            role.role_name,
            account_number))

        for policy_name, policy in repoed_policies.items():
            try:
                ca.call('iam.client.put_role_policy', RoleName=role.role_name, PolicyName=policy_name,
                        PolicyDocument=json.dumps(policy, indent=2, sort_keys=True))

            except botocore.exceptions.ClientError as e:
                error = 'Exception calling PutRolePolicy on {role}/{policy} in account {account}\n{e}\n'.format(
                    role=role.role_name, policy=policy_name, account=account_number, e=str(e))
                LOGGER.error(error)
                errors.append(error)

    current_policies = get_role_inline_policies(role.as_dict(), **conn) or {}
    roledata.add_new_policy_version(dynamo_table, role, current_policies, 'Repo')

    # regardless of whether we're successful we want to unschedule the repo
    set_role_data(dynamo_table, role.role_id, {'RepoScheduled': 0})

    repokid.hooks.call_hooks(hooks, 'AFTER_REPO', {'role': role})

    if not errors:
        # repos will stay scheduled until they are successful
        set_role_data(dynamo_table, role.role_id, {'Repoed': datetime.datetime.utcnow().isoformat()})
        _update_repoed_description(role.role_name, **conn)
        _update_role_data(role, dynamo_table, account_number, config, conn, hooks, source='Repo', add_no_repo=False)
        LOGGER.info('Successfully repoed role: {} in account {}'.format(role.role_name, account_number))
    return errors
Beispiel #17
0
def anomaly(ctx, accounts):
    """Detect anomalies in CloudTrail"""

    conn_details = {
        'account_number': ctx.config.get('aws', {}).get('organizations', {}).get('accountId', '123'),
        'assume_role': ctx.config.get('aws', {}).get('organizations', {}).get('roleName', '123'),
        'session_name': 'cloudtrail-anomaly'
    }

    ca = CloudAux(**conn_details)

    if not accounts:
        accounts = get_accounts_from_orgs(ca)
        log.info('Received {} accounts from Organizations'.format(len(accounts)))
    else:
        log.info('Received {} accounts from command line'.format(len(accounts)))

    conn_details['assume_role'] = ctx.config.get('aws', {}).get('iam', {}).get('roleName', '123')

    conn_details_athena = {
        'account_number': ctx.config['aws']['athena']['accountId'],
        'assume_role': ctx.config['aws']['athena']['roleName'],
        'session_name': 'cloudtrail-anomaly',
        'region': ctx.config.get('aws', {}).get('region', 'us-east-1')
    }
    ca_athena = CloudAux(**conn_details_athena)

    dynamodb = boto3.resource('dynamodb', region_name=ctx.config.get('aws', {}).get('region', 'us-east-1'))
    dynamo_table = dynamodb.Table(ctx.config.get('aws', {}).get('dynamoTableName', 'cloudtrail_anomaly'))

    sns_topic = boto3.client('sns', region_name=ctx.config.get('aws', {}).get('region', 'us-east-1'))

    for account in accounts:
        conn_details['account_number'] = account
        
        ca = CloudAux(**conn_details)
        
        roles = get_roles_in_account(ca)

        for role in roles:
            role_name = roles[role]['RoleName']
            new_ttl = int(time.mktime((datetime.datetime.now() + datetime.timedelta(days=ctx.config['roleAction']['dayThreshold'])).timetuple()))
            principal_id = roles[role]['RoleId']
            athena_query = "SELECT DISTINCT eventsource, eventname FROM cloudtrail_{} WHERE useridentity.type = 'AssumedRole' AND useridentity.sessioncontext.sessionissuer.principalid= '{}' AND eventTime > to_iso8601(current_timestamp - interval '1' hour);".format(account, principal_id)

            log.info('Running Athena Query for {} in {}'.format(roles[role]['RoleName'], account))
            file_name = query_athena(ctx.config, athena_query, ca_athena)

            if not file_name:
                log.error("Execution failed or timed out")
                continue

            s3_key = ctx.config.get('aws', {}).get('athena', {}).get('prefix', 'cloudtrail_anomaly') + '/' + file_name

            data = read_data_from_s3(ctx.config['aws']['athena']['bucket'], s3_key, ca_athena)

            role_actions = []

            # Remove the header and loop through calls in last hour:
            for call in data[1:]:
                service_pair = call.split(',')
                service_action = ':'.join(service_pair)
                if len(service_action) == 0:
                    continue

                log.debug('Checking DynamoDB for never seen before actions on {} in {}'.format(role_name, account))
                key = {'RoleId': principal_id, 'Action': service_action}
                response = dynamo_table.get_item(Key=key)

                if response and 'Item' in response:
                    dynamo_table.update_item(Key=key,
                                             UpdateExpression='SET #ttl = :ttl',
                                             ExpressionAttributeNames={'#ttl': 'TTL'},
                                             ExpressionAttributeValues={':ttl': new_ttl})
                else:
                    # keep track of which actions are new
                    if service_action not in ctx.config['roleAction'].get('IgnoredActionsNotify', []):
                        role_actions.append(service_action)
                        log.info('Newly seen action: {} - {} in {}'.format(role_name, service_action, account))
                    dynamo_table.put_item(Item={'RoleId': principal_id,
                                                'Action': service_action,
                                                'TTL': new_ttl})

            if len(role_actions) > 0:
                arn = role
                create_date = roles[role]['CreateDate']
                role_name = roles[role]['RoleName']
                skip_alert = False

                # if the role is too new, don't alert
                if create_date > datetime.datetime.now(pytz.utc) - datetime.timedelta(days=ctx.config['roleAction']['dayThreshold']):
                    skip_alert = True
                    log.debug('{} in {} is too new, skipping alert'.format(role_name, account))
                # if the role is a service role, don't alert
                if 'aws-service-role' in arn.split('/'):
                    skip_alert = True
                    log.debug('{} in {} is an AWS service role, skipping alert'.format(role_name, account))

                if not skip_alert:
                    log.info('Sending alert for new actions for {} in {}'.format(role_name, account))
                    alert = {
                        'actions': ', '.join(action for action in role_actions),
                        'role': role_name,
                        'account': account
                    }
                    sns_topic.publish(
                        TopicArn=ctx.config['aws']['snsTopicArn'],
                        Message=json.dumps(alert)
                    )
Beispiel #18
0
def rollback_role(account_number, role_name, selection=None, commit=None):
    role_data = _find_role_in_cache(role_name)
    if not role_data:
        LOGGER.error("Couldn't find role ({}) in cache".format(role_name))
        return

    # no option selected, display a table of options
    if not selection:
        headers = [
            'Number', 'Source', 'Discovered', 'Policy Length',
            'Policy Contents'
        ]
        rows = []
        for index, policies_version in enumerate(role_data['Policies']):
            rows.append([
                index, policies_version['Source'],
                policies_version['Discovered'],
                len(str(policies_version['Policy'])),
                str(policies_version['Policy'])[:50]
            ])
        print tabulate(rows, headers=headers)
        return

    from cloudaux import CloudAux
    conn = CONFIG['connection_iam']
    conn['account_number'] = account_number
    ca = CloudAux(**conn)

    current_policies = get_role_inline_policies(role_data, **conn)

    if selection and not commit:
        pp = pprint.PrettyPrinter()
        print "Will restore the following policies:"
        pp.pprint(role_data['Policies'][int(selection)]['Policy'])
        print "Current policies:"
        pp.pprint(current_policies)
        return

    # if we're restoring from a version with fewer policies than we have now, we need to remove them to
    # complete the restore.  To do so we'll store all the policy names we currently have and remove them
    # from the list as we update.  Any policy names left need to be manually removed
    policies_to_remove = current_policies.keys()

    for policy_name, policy in role_data['Policies'][int(
            selection)]['Policy'].items():
        try:
            LOGGER.info("Pushing cached policy: {}".format(policy_name))
            ca.call('iam.client.put_role_policy',
                    RoleName=role_data['RoleName'],
                    PolicyName=policy_name,
                    PolicyDocument=json.dumps(policy, indent=2,
                                              sort_keys=True))
        except Exception as e:
            LOGGER.error("Unable to push policy {}.  Error: {}".format(
                policy_name, e.message))
        else:
            # remove the policy name if it's in the list
            try:
                policies_to_remove.remove(policy_name)
            except:
                pass

    if policies_to_remove:
        for policy_name in policies_to_remove:
            try:
                ca.call('iam.client.delete_role_policy',
                        RoleName=role_data['RoleName'],
                        PolicyName=policy_name)
            except Exception as e:
                LOGGER.error("Unable to delete policy {}.  Error: {}".format(
                    policy_name, e.message))

    role_data['policies'] = get_role_inline_policies(role_data, **conn) or {}
    roledata.add_new_policy_version(role_data, 'Restore')
    LOGGER.info('Successfully restored selected version of role policies')
Beispiel #19
0
def repo_role(account_number, role_name, commit=False):
    role_data = _find_role_in_cache(role_name)
    if not role_data:
        LOGGER.error('Could not find role with name {}'.format(role_name))
        return

    disqualified_by = role_data.get('DisqualifiedBy', [])
    if len(disqualified_by) > 0:
        LOGGER.info(
            'Cannot repo role {} because it is being disqualified by: {}'.
            format(role_name, disqualified_by))
        return

    if 'AAData' not in role_data:
        LOGGER.warn('ARN not found in Access Advisor: {}'.format(
            role_data['Arn']))
        return

    old_aa_data_services = []
    for aa_service in role_data['AAData']:
        if (datetime.datetime.strptime(aa_service['lastUpdated'],
                                       '%a, %d %B %Y %H:%M:%S %Z') <
                datetime.datetime.now() - datetime.timedelta(
                    days=CONFIG['repo_requirements']['oldest_aa_data_days'])):
            old_aa_data_services.append(aa_service['serviceName'])
    if old_aa_data_services:
        LOGGER.error(
            'AAData older than threshold for these services: {}'.format(
                old_aa_data_services))
        return

    permissions = _get_role_permissions(role_data)
    repoable_permissions = _get_repoable_permissions(permissions, role_data)
    repoed_policies, deleted_policy_names = _get_repoed_policy(
        role_data, repoable_permissions)

    policies_length = len(json.dumps(repoed_policies))
    if not commit:
        for name in deleted_policy_names:
            LOGGER.info('Would delete policy from {} with name {}'.format(
                role_name, name))
        if repoed_policies:
            LOGGER.info('Would replace policies for role {} with: \n{}'.format(
                role_name, json.dumps(repoed_policies,
                                      indent=2,
                                      sort_keys=True)))
        if policies_length > MAX_AWS_POLICY_SIZE:
            LOGGER.error(
                "Policies would exceed the AWS size limit after repo for role: {}.  "
                "Please manually minify.".format(role_name))
        return

    from cloudaux import CloudAux
    conn = CONFIG['connection_iam']
    conn['account_number'] = account_number
    ca = CloudAux(**conn)

    if policies_length > MAX_AWS_POLICY_SIZE:
        LOGGER.error(
            "Policies would exceed the AWS size limit after repo for role: {}.  "
            "Please manually minify.".format(role_name))
        return

    for name in deleted_policy_names:
        LOGGER.info('Deleting policy with name {} from {}'.format(
            name, role_data['RoleName']))
        ca.call('iam.client.delete_role_policy',
                RoleName=role_data['RoleName'],
                PolicyName=name)

    if repoed_policies:
        LOGGER.info('Replacing Policies With: \n{}'.format(
            json.dumps(repoed_policies, indent=2, sort_keys=True)))
        for policy_name, policy in repoed_policies.items():
            try:
                ca.call('iam.client.put_role_policy',
                        RoleName=role_data['RoleName'],
                        PolicyName=policy_name,
                        PolicyDocument=json.dumps(policy,
                                                  indent=2,
                                                  sort_keys=True))
            except Exception as e:
                LOGGER.error(
                    'Exception calling PutRolePolicy on {role}/{policy}\n{e}\n'
                    .format(role=role_data['RoleName'],
                            policy=policy_name,
                            e=str(e)))
                return

    role_data['policies'] = get_role_inline_policies(role_data, **conn) or {}
    roledata.add_new_policy_version(role_data, 'Repo')
    roledata.set_repoed(role_data['RoleId'])

    # update total permissions count for stats
    permissions_count = len(_get_role_permissions(role_data))
    roledata.update_total_permissions(role_data['RoleId'], permissions_count)

    roledata.update_stats(source='Repo', roleID=role_data['RoleId'])

    LOGGER.info('Successfully repoed role: {}'.format(role_data['RoleName']))
Beispiel #20
0
def rollback_role(account_number, role_name, selection=None, commit=None):
    """
    Display the historical policy versions for a roll as a numbered list.  Restore to a specific version if selected.
    Indicate changes that will be made and then actually make them if commit is selected.

    Args:
        account_number (string)
        role_name (string)
        selection (int): which policy version in the list to rollback to
        commit (bool): actually make the change

    Returns:
        None
    """
    role_data = _find_role_in_cache(account_number, role_name)
    if not role_data:
        LOGGER.warn('Could not find role with name {}'.format(role_name))
        return
    else:
        role = Role(role_data)

    # no option selected, display a table of options
    if not selection:
        headers = [
            'Number', 'Source', 'Discovered', 'Policy Length',
            'Policy Contents'
        ]
        rows = []
        for index, policies_version in enumerate(role.policies):
            rows.append([
                index, policies_version['Source'],
                policies_version['Discovered'],
                len(str(policies_version['Policy'])),
                str(policies_version['Policy'])[:50]
            ])
        print tabulate(rows, headers=headers)
        return

    from cloudaux import CloudAux
    conn = CONFIG['connection_iam']
    conn['account_number'] = account_number
    ca = CloudAux(**conn)

    current_policies = get_role_inline_policies(role.as_dict(), **conn)

    if selection and not commit:
        pp = pprint.PrettyPrinter()
        print "Will restore the following policies:"
        pp.pprint(role.policies[int(selection)]['Policy'])
        print "Current policies:"
        pp.pprint(current_policies)
        return

    # if we're restoring from a version with fewer policies than we have now, we need to remove them to
    # complete the restore.  To do so we'll store all the policy names we currently have and remove them
    # from the list as we update.  Any policy names left need to be manually removed
    policies_to_remove = current_policies.keys()

    for policy_name, policy in role.policies[int(selection)]['Policy'].items():
        try:
            LOGGER.info("Pushing cached policy: {}".format(policy_name))
            ca.call('iam.client.put_role_policy',
                    RoleName=role.role_name,
                    PolicyName=policy_name,
                    PolicyDocument=json.dumps(policy, indent=2,
                                              sort_keys=True))

        except botocore.excpetion.ClientError as e:
            LOGGER.error("Unable to push policy {}.  Error: {}".format(
                policy_name, e.message))

        else:
            # remove the policy name if it's in the list
            try:
                policies_to_remove.remove(policy_name)
            except:
                pass

    if policies_to_remove:
        for policy_name in policies_to_remove:
            try:
                ca.call('iam.client.delete_role_policy',
                        RoleName=role.role_name,
                        PolicyName=policy_name)

            except botocore.excpetion.ClientError as e:
                LOGGER.error("Unable to delete policy {}.  Error: {}".format(
                    policy_name, e.message))

    # TODO: possibly update the total and repoable permissions here, we'd have to get Aardvark and all that

    current_policies = get_role_inline_policies(role.as_dict(), **conn) or {}
    roledata.add_new_policy_version(role, current_policies, 'Restore')
    role.total_permissions = len(roledata._get_role_permissions(role))

    # update stats
    roledata.update_stats([role], source='Restore')

    LOGGER.info('Successfully restored selected version of role policies')
Beispiel #21
0
def repo_role(account_number, role_name, commit=False):
    """
    Calculate what repoing can be done for a role and then actually do it if commit is set
      1) Check that a role exists, it isn't being disqualified by a filter, and that is has fresh AA data
      2) Get the role's current permissions, repoable permissions, and the new policy if it will change
      3) Make the changes if commit is set
    Args:
        account_number (string)
        role_name (string)
        commit (bool)

    Returns:
        None
    """
    errors = []

    role_data = _find_role_in_cache(account_number, role_name)
    if not role_data:
        LOGGER.warn('Could not find role with name {}'.format(role_name))
        return
    else:
        role = Role(role_data)

    if len(role.disqualified_by) > 0:
        LOGGER.info(
            'Cannot repo role {} because it is being disqualified by: {}'.
            format(role_name, role.disqualified_by))
        return

    if not role.aa_data:
        LOGGER.warn('ARN not found in Access Advisor: {}'.format(role.arn))
        return

    if not role.repoable_permissions:
        LOGGER.info('No permissions to repo for role {}'.format(role_name))
        return

    old_aa_data_services = []
    for aa_service in role.aa_data:
        if (datetime.datetime.strptime(aa_service['lastUpdated'],
                                       '%a, %d %b %Y %H:%M:%S %Z') <
                datetime.datetime.now() - datetime.timedelta(
                    days=CONFIG['repo_requirements']['oldest_aa_data_days'])):
            old_aa_data_services.append(aa_service['serviceName'])

    if old_aa_data_services:
        LOGGER.error(
            'AAData older than threshold for these services: {}'.format(
                old_aa_data_services))
        return

    permissions = roledata._get_role_permissions(role)
    repoable_permissions = roledata._get_repoable_permissions(
        permissions, role.aa_data, role.no_repo_permissions)
    repoed_policies, deleted_policy_names = roledata._get_repoed_policy(
        role.policies[-1]['Policy'], repoable_permissions)

    policies_length = len(json.dumps(repoed_policies))
    if not commit:
        for name in deleted_policy_names:
            LOGGER.info('Would delete policy from {} with name {}'.format(
                role_name, name))
        if repoed_policies:
            LOGGER.info('Would replace policies for role {} with: \n{}'.format(
                role_name, json.dumps(repoed_policies,
                                      indent=2,
                                      sort_keys=True)))
        if policies_length > MAX_AWS_POLICY_SIZE:
            LOGGER.error(
                "Policies would exceed the AWS size limit after repo for role: {}.  "
                "Please manually minify.".format(role_name))
        return

    from cloudaux import CloudAux
    conn = CONFIG['connection_iam']
    conn['account_number'] = account_number
    ca = CloudAux(**conn)

    if policies_length > MAX_AWS_POLICY_SIZE:
        LOGGER.error(
            "Policies would exceed the AWS size limit after repo for role: {}.  "
            "Please manually minify.".format(role_name))
        return

    for name in deleted_policy_names:
        LOGGER.info('Deleting policy with name {} from {}'.format(
            name, role.role_name))
        try:
            ca.call('iam.client.delete_role_policy',
                    RoleName=role.role_name,
                    PolicyName=name)
        except botocore.exceptions.ClientError as e:
            error = 'Error deleting policy: {} from role: {}.  Exception: {}'.format(
                name, role.role_name, e)
            LOGGER.error(error)
            errors.append(error)

    if repoed_policies:
        LOGGER.info('Replacing Policies With: \n{}'.format(
            json.dumps(repoed_policies, indent=2, sort_keys=True)))
        for policy_name, policy in repoed_policies.items():
            try:
                ca.call('iam.client.put_role_policy',
                        RoleName=role.role_name,
                        PolicyName=policy_name,
                        PolicyDocument=json.dumps(policy,
                                                  indent=2,
                                                  sort_keys=True))

            except botocore.exceptions.ClientError as e:
                error = 'Exception calling PutRolePolicy on {role}/{policy}\n{e}\n'.format(
                    role=role.role_name, policy=policy_name, e=str(e))
                LOGGER.error(error)
                errors.append(error)

    current_policies = get_role_inline_policies(role.as_dict(), **conn) or {}
    roledata.add_new_policy_version(role, current_policies, 'Repo')

    if not errors:
        roledata.set_repoed(role.role_id)

        # update total and repoable permissions and services
        role.total_permissions = len(roledata._get_role_permissions(role))
        role.repoable_permissions = 0
        role.repoable_services = []
        roledata.update_repoable_data([role])

        # update stats
        roledata.update_stats([role], source='Repo')

        LOGGER.info('Successfully repoed role: {}'.format(role.role_name))
    return errors