Beispiel #1
0
    def __init__(self, config=None):
        blocklist_json = None
        bucket_config = config.get(
            "blocklist_bucket", config.get("blacklist_bucket", None)
        )
        if bucket_config:
            blocklist_json = get_blocklist_from_bucket(bucket_config)

        current_account = config.get("current_account") or None
        if not current_account:
            LOGGER.error("Unable to get current account for Blocklist Filter")

        blocklisted_role_names = set()
        blocklisted_role_names.update(
            [rolename.lower() for rolename in config.get(current_account, [])]
        )
        blocklisted_role_names.update(
            [rolename.lower() for rolename in config.get("all", [])]
        )

        if blocklist_json:
            blocklisted_role_names.update(
                [
                    name.lower()
                    for name, accounts in blocklist_json["names"].items()
                    if ("all" in accounts or config.get("current_account") in accounts)
                ]
            )

        self.blocklisted_arns = (
            set() if not blocklist_json else blocklist_json.get("arns", [])
        )
        self.blocklisted_role_names = blocklisted_role_names
Beispiel #2
0
def update_repoed_description(role_name, client=None):
    description = None
    try:
        description = client.get_role(RoleName=role_name)["Role"].get(
            "Description", "")
    except KeyError:
        return
    date_string = datetime.datetime.utcnow().strftime("%m/%d/%y")
    if "; Repokid repoed" in description:
        new_description = re.sub(
            r"; Repokid repoed [0-9]{2}\/[0-9]{2}\/[0-9]{2}",
            "; Repokid repoed {}".format(date_string),
            description,
        )
    else:
        new_description = description + " ; Repokid repoed {}".format(
            date_string)
    # IAM role descriptions have a max length of 1000, if our new length would be longer, skip this
    if len(new_description) < 1000:
        client.update_role_description(RoleName=role_name,
                                       Description=new_description)
    else:
        LOGGER.error(
            "Unable to set repo description ({}) for role {}, length would be too long"
            .format(new_description, role_name))
Beispiel #3
0
    def __init__(self, config=None):
        blocklist_json = None
        bucket_config = config.get('blocklist_bucket',
                                   config.get('blacklist_bucket', None))
        if bucket_config:
            blocklist_json = get_blocklist_from_bucket(bucket_config)

        current_account = config.get('current_account') or None
        if not current_account:
            LOGGER.error('Unable to get current account for Blocklist Filter')

        blocklisted_role_names = set()
        blocklisted_role_names.update(
            [rolename.lower() for rolename in config.get(current_account, [])])
        blocklisted_role_names.update(
            [rolename.lower() for rolename in config.get('all', [])])

        if blocklist_json:
            blocklisted_role_names.update([
                name.lower()
                for name, accounts in blocklist_json['names'].items()
                if ('all' in accounts
                    or config.get('current_account') in accounts)
            ])

        self.blocklisted_arns = set(
        ) if not blocklist_json else blocklist_json.get('arns', [])
        self.blocklisted_role_names = blocklisted_role_names
Beispiel #4
0
def update_aardvark_data(aardvark_data, roles):
    """
    Update Aardvark data for a given set of roles by looking for the ARN in the aardvark data dict.
    If the ARN is in Aardvark data update the role's aa_data attribute and Dynamo.

    Args:
        aardvark_data (dict): A dict of Aardvark data from an account
        roles (Roles): a list of all the role objects to update data for

    Returns:
        None
    """
    for role in roles:
        if role.arn in aardvark_data:
            role.aa_data = aardvark_data[role.arn]
        try:
            DYNAMO_TABLE.update_item(Key={'RoleId': role.role_id},
                                     UpdateExpression="SET AAData=:aa_data",
                                     ExpressionAttributeValues={
                                         ":aa_data":
                                         _empty_string_to_dynamo_replace(
                                             role.aa_data)
                                     })
        except BotoClientError as e:
            LOGGER.error('Dynamo table error: {}'.format(e))
Beispiel #5
0
def update_stats(roles, source='Scan'):
    """
    Create a new stats entry for each role in a set of roles and add it to Dynamo

    Args:
        roles (Roles): a list of all the role objects to update data for
        source (string): the source of the new stats data (repo, scan, etc)

    Returns:
        None
    """
    for role in roles:
        cur_stats = {
            'Date': datetime.datetime.utcnow().isoformat(),
            'DisqualifiedBy': role.disqualified_by,
            'PermissionsCount': role.total_permissions,
            'Source': source
        }

        try:
            DYNAMO_TABLE.update_item(
                Key={'RoleId': role.role_id},
                UpdateExpression=("SET #statsarray = list_append(if_not_exists"
                                  "(#statsarray, :empty_list), :stats)"),
                ExpressionAttributeNames={"#statsarray": "Stats"},
                ExpressionAttributeValues={
                    ":empty_list": [],
                    ":stats": [cur_stats]
                })

        except BotoClientError as e:
            LOGGER.error('Dynamo table error: {}'.format(e))
Beispiel #6
0
def cancel_scheduled_repo(account_number, dynamo_table, role_name=None, is_all=None):
    """
    Cancel scheduled repo for a role in an account
    """
    if not is_all and not role_name:
        LOGGER.error('Either a specific role to cancel or all must be provided')
        return

    if is_all:
        roles = Roles([Role(get_role_data(dynamo_table, roleID))
                      for roleID in role_ids_for_account(dynamo_table, account_number)])

        # filter to show only roles that are scheduled
        roles = [role for role in roles if (role.repo_scheduled)]

        for role in roles:
            set_role_data(dynamo_table, role.role_id, {'RepoScheduled': 0, 'ScheduledPerms': []})

        LOGGER.info('Canceled scheduled repo for roles: {}'.format(', '.join([role.role_name for role in roles])))
        return

    role_id = find_role_in_cache(dynamo_table, account_number, role_name)
    if not role_id:
        LOGGER.warn('Could not find role with name {} in account {}'.format(role_name, account_number))
        return

    role = Role(get_role_data(dynamo_table, role_id))

    if not role.repo_scheduled:
        LOGGER.warn('Repo was not scheduled for role {} in account {}'.format(role.role_name, account_number))
        return

    set_role_data(dynamo_table, role.role_id, {'RepoScheduled': 0, 'ScheduledPerms': []})
    LOGGER.info('Successfully cancelled scheduled repo for role {} in account {}'.format(role.role_name,
                role.account))
Beispiel #7
0
def repo_stats(output_file, dynamo_table, account_number=None):
    """
    Create a csv file with stats about roles, total permissions, and applicable filters over time

    Args:
        output_file (string): the name of the csv file to write
        account_number (string): if specified only display roles from selected account, otherwise display all

    Returns:
        None
    """
    roleIDs = (role_ids_for_account(dynamo_table, account_number) if account_number else
               role_ids_for_all_accounts(dynamo_table))
    headers = ['RoleId', 'Role Name', 'Account', 'Active', 'Date', 'Source', 'Permissions Count',
               'Repoable Permissions Count', 'Disqualified By']
    rows = []

    for roleID in roleIDs:
        role_data = get_role_data(dynamo_table, roleID, fields=['RoleId', 'RoleName', 'Account', 'Active', 'Stats'])
        for stats_entry in role_data.get('Stats', []):
            rows.append([role_data['RoleId'], role_data['RoleName'], role_data['Account'], role_data['Active'],
                         stats_entry['Date'], stats_entry['Source'], stats_entry['PermissionsCount'],
                         stats_entry.get('RepoablePermissionsCount'), stats_entry.get('DisqualifiedBy', [])])

    try:
        with open(output_file, 'wb') as csvfile:
            csv_writer = csv.writer(csvfile)
            csv_writer.writerow(headers)
            for row in rows:
                csv_writer.writerow(row)
    except IOError as e:
        LOGGER.error('Unable to write file {}: {}'.format(output_file, e))
    else:
        LOGGER.info('Successfully wrote stats to {}'.format(output_file))
Beispiel #8
0
def _get_role_data(roleID, fields=None):
    """
    Get raw role data as a dictionary for a given role by ID
    Do not use for data presented to the user because this data still has dynamo empty string placeholders, use
    get_role_data() instead

    Args:
        roleID (string)

    Returns:
        dict: data for the role if it exists, else None
    """
    try:
        if fields:
            response = DYNAMO_TABLE.get_item(Key={'RoleId': roleID},
                                             AttributesToGet=fields)
        else:
            response = DYNAMO_TABLE.get_item(Key={'RoleId': roleID})
    except BotoClientError as e:
        LOGGER.error('Dynamo table error: {}'.format(e))
    else:
        if 'Item' in response:
            return response['Item']
        else:
            return None
Beispiel #9
0
def role_ids_for_all_accounts():
    """
    Get a list of all role IDs for all accounts by scanning the Dynamo table

    Args:
        None

    Returns:
        list: role ids in all accounts
    """
    role_ids = []

    try:
        response = DYNAMO_TABLE.scan(ProjectionExpression='RoleId')
        role_ids.extend(
            [role_dict['RoleId'] for role_dict in response['Items']])

        while 'LastEvaluatedKey' in response:
            response = DYNAMO_TABLE.scan(
                ProjectionExpression='RoleId',
                ExclusiveStartKey=response['LastEvaluatedKey'])
            role_ids.extend(
                [role_dict['RoleId'] for role_dict in response['Items']])
    except BotoClientError as e:
        LOGGER.error('Dynamo table error: {}'.format(e))

    return role_ids
Beispiel #10
0
def role_ids_for_account(account_number):
    """
    Get a list of all role IDs in a given account by querying the Dynamo secondary index 'account'

    Args:
        account_number (string)

    Returns:
        list: role ids in given account
    """
    role_ids = set()
    try:
        results = DYNAMO_TABLE.query(
            IndexName='Account',
            ProjectionExpression='RoleId',
            KeyConditionExpression='Account = :act',
            ExpressionAttributeValues={':act': account_number})
        role_ids.update(
            [return_dict['RoleId'] for return_dict in results.get('Items')])

        while 'LastEvaluatedKey' in results:
            results = DYNAMO_TABLE.query(
                IndexName='Account',
                ProjectionExpression='RoleId',
                KeyConditionExpression='Account = :act',
                ExpressionAttributeValues={':act': account_number},
                ExclusiveStartKey=results.get('LastEvaluatedKey'))
            role_ids.update([
                return_dict['RoleId'] for return_dict in results.get('Items')
            ])
    except BotoClientError as e:
        LOGGER.error('Dynamo table error: {}'.format(e))

    return role_ids
Beispiel #11
0
def find_and_mark_inactive(account_number, active_roles):
    """
    Mark roles in the account that aren't currently active inactive. Do this by getting all roles in the account and
    subtracting the active roles, any that are left are inactive and should be marked thusly.

    Args:
        account_number (string)
        active_roles (set): the currently active roles discovered in the most recent scan

    Returns:
        None
    """

    active_roles = set(active_roles)
    known_roles = set(role_ids_for_account(account_number))
    inactive_roles = known_roles - active_roles

    for roleID in inactive_roles:
        role_dict = _get_role_data(roleID, fields=['Active', 'Arn'])
        if role_dict['Active']:
            try:
                DYNAMO_TABLE.update_item(
                    Key={'RoleId': roleID},
                    UpdateExpression="SET Active = :false",
                    ExpressionAttributeValues={":false": False})
            except BotoClientError as e:
                LOGGER.error('Dynamo table error: {}'.format(e))
            else:
                LOGGER.info('Marked role ({}): {} inactive'.format(
                    roleID, role_dict['Arn']))
Beispiel #12
0
def _get_potentially_repoable_permissions(role_name, account_number, aa_data,
                                          permissions, no_repo_permissions,
                                          minimum_age):
    ago = datetime.timedelta(minimum_age)
    now = datetime.datetime.now(tzlocal())

    current_time = time.time()
    no_repo_list = [
        perm.lower() for perm in no_repo_permissions
        if no_repo_permissions[perm] > current_time
    ]

    # cast all permissions to lowercase
    permissions = [permission.lower() for permission in permissions]
    potentially_repoable_permissions = {
        permission: RepoablePermissionDecision()
        for permission in permissions if permission not in no_repo_list
    }

    used_services = set()
    for service in aa_data:
        (accessed, valid_authenticated) = _get_epoch_authenticated(
            service["lastAuthenticated"])

        if not accessed:
            continue

        if not valid_authenticated:
            LOGGER.error(
                "Got malformed Access Advisor data for {role_name} in {account_number} for service {service}"
                ": {last_authenticated}".format(
                    role_name=role_name,
                    account_number=account_number,
                    service=service.get("serviceNamespace"),
                    last_authenticated=service["lastAuthenticated"],
                ))
            used_services.add(service["serviceNamespace"])

        accessed = datetime.datetime.fromtimestamp(accessed, tzlocal())
        if accessed > now - ago:
            used_services.add(service["serviceNamespace"])

    for permission_name, permission_decision in list(
            potentially_repoable_permissions.items()):
        if permission_name.split(
                ":")[0] in IAM_ACCESS_ADVISOR_UNSUPPORTED_SERVICES:
            LOGGER.warn("skipping {}".format(permission_name))
            continue

        # we have an unused service but need to make sure it's repoable
        if permission_name.split(":")[0] not in used_services:
            if permission_name in IAM_ACCESS_ADVISOR_UNSUPPORTED_ACTIONS:
                LOGGER.warn("skipping {}".format(permission_name))
                continue

            permission_decision.repoable = True
            permission_decision.decider = "Access Advisor"

    return potentially_repoable_permissions
Beispiel #13
0
def repo_all_roles(account_number,
                   dynamo_table,
                   config,
                   hooks,
                   commit=False,
                   scheduled=True):
    """
    Repo all scheduled or eligible roles in an account.  Collect any errors and display them at the end.

    Args:
        account_number (string)
        dynamo_table
        config
        commit (bool): actually make the changes
        scheduled (bool): if True only repo the scheduled roles, if False repo all the (eligible) roles

    Returns:
        None
    """
    errors = []

    role_ids_in_account = role_ids_for_account(dynamo_table, account_number)
    roles = Roles([])
    for role_id in role_ids_in_account:
        roles.append(
            Role(
                get_role_data(dynamo_table,
                              role_id,
                              fields=['Active', 'RoleName', 'RepoScheduled'])))

    roles = roles.filter(active=True)

    cur_time = int(time.time())
    if scheduled:
        roles = [
            role for role in roles
            if (role.repo_scheduled and cur_time > role.repo_scheduled)
        ]

    LOGGER.info('Repoing these {}roles from account {}:\n\t{}'.format(
        'scheduled ' if scheduled else '', account_number,
        ', '.join([role.role_name for role in roles])))

    for role in roles:
        error = repo_role(account_number,
                          role.role_name,
                          dynamo_table,
                          config,
                          hooks,
                          commit=commit)
        if error:
            errors.append(error)

    if errors:
        LOGGER.error('Error(s) during repo: \n{}'.format(errors))
    else:
        LOGGER.info('Everything successful!')
Beispiel #14
0
    def __init__(self, config=None):
        current_account = config.get("current_account") or None
        if not current_account:
            LOGGER.error("Unable to get current account for Exclusive Filter")

        exclusive_role_globs = set()
        exclusive_role_globs.update([
            role_glob.lower() for role_glob in config.get(current_account, [])
        ])
        exclusive_role_globs.update(
            [role_glob.lower() for role_glob in config.get("all", [])])

        self.exclusive_role_globs = exclusive_role_globs
Beispiel #15
0
def get_aardvark_data(aardvark_api_location, account_number=None, arn=None):
    """
    Make a request to the Aardvark server to get all data about a given account or ARN.
    We'll request in groups of PAGE_SIZE and check the current count to see if we're done. Keep requesting as long as
    the total count (reported by the API) is greater than the number of pages we've received times the page size.  As
    we go, keeping building the dict and return it when done.

    Args:
        aardvark_api_location
        account_number (string): Used to form the phrase query for Aardvark so we only get data for the account we want
        arn (string)

    Returns:
        dict: Aardvark data is a dict with the role ARN as the key and a list of services as value
    """
    response_data = {}

    PAGE_SIZE = 1000
    page_num = 1

    if account_number:
        payload = {"phrase": "{}".format(account_number)}
    elif arn:
        payload = {"arn": [arn]}
    else:
        return
    while True:
        params = {"count": PAGE_SIZE, "page": page_num}
        try:
            r_aardvark = requests.post(
                aardvark_api_location, params=params, json=payload
            )
        except requests.exceptions.RequestException as e:
            LOGGER.error("Unable to get Aardvark data: {}".format(e))
            sys.exit(1)
        else:
            if r_aardvark.status_code != 200:
                LOGGER.error("Unable to get Aardvark data")
                sys.exit(1)

            response_data.update(r_aardvark.json())
            # don't want these in our Aardvark data
            response_data.pop("count")
            response_data.pop("page")
            response_data.pop("total")
            if PAGE_SIZE * page_num < r_aardvark.json().get("total"):
                page_num += 1
            else:
                break
    return response_data
Beispiel #16
0
def _update_repoed_description(role_name, client=None):
    description = None
    try:
        description = client.get_role(RoleName=role_name)['Role'].get('Description', '')
    except KeyError:
        return
    date_string = datetime.datetime.utcnow().strftime('%m/%d/%y')
    if '; Repokid repoed' in description:
        new_description = re.sub(r'; Repokid repoed [0-9]{2}\/[0-9]{2}\/[0-9]{2}', '; Repokid repoed {}'.format(
                                 date_string), description)
    else:
        new_description = description + ' ; Repokid repoed {}'.format(date_string)
    # IAM role descriptions have a max length of 1000, if our new length would be longer, skip this
    if len(new_description) < 1000:
        client.update_role_description(RoleName=role_name, Description=new_description)
    else:
        LOGGER.error('Unable to set repo description ({}) for role {}, length would be too long'.format(
            new_description, role_name))
Beispiel #17
0
def update_filtered_roles(roles):
    """
    Update the disqualified by (applicable filters) in Dynamo for each role in a list of roles

    Args:
        roles (Roles)

    Returns:
        None
    """
    for role in roles:
        try:
            DYNAMO_TABLE.update_item(
                Key={'RoleId': role.role_id},
                UpdateExpression="SET DisqualifiedBy = :dqby",
                ExpressionAttributeValues={":dqby": role.disqualified_by})
        except BotoClientError as e:
            LOGGER.error('Dynamo table error: {}'.format(e))
Beispiel #18
0
def update_opt_out(role):
    """
    Update opt-out object for a role - remove (set to empty dict) any entries that have expired
    Opt-out objects should have the form {'expire': xxx, 'owner': xxx, 'reason': xxx}

    Args:
        role

    Returns:
        None
    """
    if role.opt_out and int(role.opt_out['expire']) < int(time.time()):
        try:
            DYNAMO_TABLE.update_item(Key={'RoleId': role.role_id},
                                     UpdateExpression="SET OptOut=:oo",
                                     ExpressionAttributeValues={":oo": {}})
        except BotoClientError as e:
            LOGGER.error('Dynamo table error: {}'.format(e))
Beispiel #19
0
def _refresh_updated_time(roleID):
    """
    Update refreshed time for given role ID to utcnow

    Args:
        rolesID (string): the role ID of the role to update

    Returns:
        None
    """
    try:
        DYNAMO_TABLE.update_item(Key={'RoleId': roleID},
                                 UpdateExpression="SET Refreshed = :cur_time",
                                 ExpressionAttributeValues={
                                     ":cur_time":
                                     datetime.datetime.utcnow().isoformat()
                                 })
    except BotoClientError as e:
        LOGGER.error('Dynamo table error: {}'.format(e))
Beispiel #20
0
def set_repoed(role_id):
    """
    Marks a role (by ID) as having been repoed now (utcnow) as string in Dynamo

    Args:
        role_id (string)

    Returns:
        None
    """
    try:
        DYNAMO_TABLE.update_item(
            Key={'RoleId': role_id},
            UpdateExpression="SET Repoed = :now, RepoableServices = :el",
            ExpressionAttributeValues={
                ":now": datetime.datetime.utcnow().isoformat(),
                ":el": []
            })
    except BotoClientError as e:
        LOGGER.error('Dynamo table error: {}'.format(e))
Beispiel #21
0
def update_no_repo_permissions(role, newly_added_permissions):
    """
    Update Dyanmo entry for newly added permissions. Any that were newly detected get added with an expiration
    date of now plus the config setting for 'repo_requirements': 'exclude_new_permissions_for_days'. Expired entries
    get deleted. Also update the role object with the new no-repo-permissions.

    Args:
        role
        newly_added_permissions (set)

    Returns:
        None
    """
    current_ignored_permissions = _get_role_data(
        role.role_id,
        fields=['NoRepoPermissions']).get('NoRepoPermissions', {})
    new_ignored_permissions = {}

    current_time = int(time.time())
    new_perms_expire_time = current_time + (
        24 * 60 * 60 * CONFIG['repo_requirements'].get(
            'exclude_new_permissions_for_days', 14))

    # only copy non-expired items to the new dictionary
    for permission, expire_time in current_ignored_permissions.items():
        if expire_time > current_time:
            new_ignored_permissions[permission] = current_ignored_permissions[
                permission]

    for permission in newly_added_permissions:
        new_ignored_permissions[permission] = new_perms_expire_time

    role.no_repo_permissions = new_ignored_permissions

    try:
        DYNAMO_TABLE.update_item(
            Key={'RoleId': role.role_id},
            UpdateExpression="SET NoRepoPermissions=:nrp",
            ExpressionAttributeValues={":nrp": new_ignored_permissions})
    except BotoClientError as e:
        LOGGER.error('Dynamo table error: {}'.format(e))
Beispiel #22
0
def remove_permissions_from_roles(
    permissions, role_filename, dynamo_table, config, hooks, commit=False
):
    """Loads roles specified in file and calls _remove_permissions_from_role() for each one.

    Args:
        permissions (list<string>)
        role_filename (string)
        commit (bool)

    Returns:
        None
    """
    roles = list()
    with open(role_filename, "r") as fd:
        roles = json.load(fd)

    for role_arn in tqdm(roles):
        arn = ARN(role_arn)
        if arn.error:
            LOGGER.error("INVALID ARN: {arn}".format(arn=role_arn))
            return

        account_number = arn.account_number
        role_name = arn.name.split("/")[-1]

        role_id = find_role_in_cache(dynamo_table, account_number, role_name)
        role = Role(get_role_data(dynamo_table, role_id))

        remove_permissions_from_role(
            account_number,
            permissions,
            role,
            role_id,
            dynamo_table,
            config,
            hooks,
            commit=commit,
        )

        repokid.hooks.call_hooks(hooks, "AFTER_REPO", {"role": role})
Beispiel #23
0
def get_blocklist_from_bucket(bucket_config):
    try:
        s3_resource = boto3_cached_conn(
            "s3",
            service_type="resource",
            account_number=bucket_config.get("account_number"),
            assume_role=bucket_config.get("assume_role", None),
            session_name="repokid",
            region=bucket_config.get("region", "us-west-2"),
        )

        s3_obj = s3_resource.Object(
            bucket_name=bucket_config["bucket_name"], key=bucket_config["key"]
        )
        blocklist = s3_obj.get()["Body"].read().decode("utf-8")
        blocklist_json = json.loads(blocklist)
    # Blocklist problems are really bad and we should quit rather than silently continue
    except (botocore.exceptions.ClientError, AttributeError):
        LOGGER.error(
            "S3 blocklist config was set but unable to connect retrieve object, quitting"
        )
        sys.exit(1)
    except ValueError:
        LOGGER.error(
            "S3 blocklist config was set but the returned file is bad, quitting"
        )
        sys.exit(1)
    if set(blocklist_json.keys()) != set(["arns", "names"]):
        LOGGER.error("S3 blocklist file is malformed, quitting")
        sys.exit(1)
    return blocklist_json
Beispiel #24
0
def get_blocklist_from_bucket(bucket_config):
    try:
        s3_resource = boto3_cached_conn(
            's3',
            service_type='resource',
            account_number=bucket_config.get('account_number'),
            assume_role=bucket_config.get('assume_role', None),
            session_name='repokid',
            region=bucket_config.get('region', 'us-west-2'))

        s3_obj = s3_resource.Object(bucket_name=bucket_config['bucket_name'],
                                    key=bucket_config['key'])
        blocklist = s3_obj.get()['Body'].read().decode("utf-8")
        blocklist_json = json.loads(blocklist)
    # Blocklist problems are really bad and we should quit rather than silently continue
    except (botocore.exceptions.ClientError, AttributeError):
        LOGGER.error(
            "S3 blocklist config was set but unable to connect retrieve object, quitting"
        )
        sys.exit(1)
    except ValueError:
        LOGGER.error(
            "S3 blocklist config was set but the returned file is bad, quitting"
        )
        sys.exit(1)
    if set(blocklist_json.keys()) != set(['arns', 'names']):
        LOGGER.error("S3 blocklist file is malformed, quitting")
        sys.exit(1)
    return blocklist_json
Beispiel #25
0
def add_new_policy_version(role, current_policy, update_source):
    """
    Create a new entry in the history of policy versions in Dynamo. The entry contains the source of the new policy:
    (scan, repo, or restore) the current time, and the current policy contents. Updates the role's policies with the
    full policies including the latest.

    Args:
        role (Role)
        current_policy (dict)
        update_source (string): ['Repo', 'Scan', 'Restore']

    Returns:
        None
    """
    cur_role_data = _get_role_data(role.role_id, fields=['Policies'])
    new_item_index = len(cur_role_data.get('Policies', []))

    try:
        policy_entry = {
            'Source': update_source,
            'Discovered': datetime.datetime.utcnow().isoformat(),
            'Policy': current_policy
        }

        DYNAMO_TABLE.update_item(
            Key={'RoleId': role.role_id},
            UpdateExpression="SET #polarray[{}] = :pol".format(new_item_index),
            ExpressionAttributeNames={"#polarray": "Policies"},
            ExpressionAttributeValues={
                ":pol": _empty_string_to_dynamo_replace(policy_entry)
            })

    except BotoClientError as e:
        LOGGER.error('Dynamo table error: {}'.format(e))

    role.policies = get_role_data(role.role_id,
                                  fields=['Policies'])['Policies']
Beispiel #26
0
def _store_item(role, current_policy):
    """
    Store the initial version of a role in Dynamo

    Args:
        role (Role)
        current_policy (dict)

    Returns:
        None
    """
    policy_entry = {
        'Source': 'Scan',
        'Discovered': datetime.datetime.utcnow().isoformat(),
        'Policy': current_policy
    }

    role.policies = [policy_entry]
    role.refreshed = datetime.datetime.utcnow().isoformat()
    role.active = True
    role.repoed = 'Never'

    try:
        DYNAMO_TABLE.put_item(
            Item={
                'Arn': role.arn,
                'CreateDate': role.create_date.isoformat(),
                'RoleId': role.role_id,
                'RoleName': role.role_name,
                'Account': role.account,
                'Policies': [_empty_string_to_dynamo_replace(policy_entry)],
                'Refreshed': role.refreshed,
                'Active': role.active,
                'Repoed': role.repoed
            })
    except BotoClientError as e:
        LOGGER.error('Dynamo table error: {}'.format(e))
Beispiel #27
0
def update_repoable_data(roles):
    """
    Update total permissions and repoable permissions count and a list of repoable services in Dynamo for each role

    Args:
        roles (Roles): a list of all the role objects to update data for

    Returns:
        None
    """
    for role in roles:
        try:
            DYNAMO_TABLE.update_item(
                Key={'RoleId': role.role_id},
                UpdateExpression=(
                    "SET TotalPermissions=:tp, RepoablePermissions=:rp, "
                    "RepoableServices=:rs"),
                ExpressionAttributeValues={
                    ":tp": role.total_permissions,
                    ":rp": role.repoable_permissions,
                    ":rs": role.repoable_services
                })
        except BotoClientError as e:
            LOGGER.error('Dynamo table error: {}'.format(e))
Beispiel #28
0
def dynamo_get_or_create_table(**dynamo_config):
    """
    Create a new table or get a reference to an existing Dynamo table named 'repokid_roles' that will store data all
    data for Repokid.  Return a table with a reference to the dynamo resource

    Args:
        dynamo_config (kwargs):
            account_number (string)
            assume_role (string) optional
            session_name (string)
            region (string)
            endpoint (string)

    Returns:
        dynamo_table object
    """
    if 'localhost' in dynamo_config['endpoint']:
        resource = boto3.resource('dynamodb',
                                  region_name='us-east-1',
                                  endpoint_url=dynamo_config['endpoint'])
    else:
        resource = boto3_cached_conn(
            'dynamodb',
            service_type='resource',
            account_number=dynamo_config['account_number'],
            assume_role=dynamo_config.get('assume_role', None),
            session_name=dynamo_config['session_name'],
            region=dynamo_config['region'])

    for table in resource.tables.all():
        if table.name == 'repokid_roles':
            return table

    table = None
    try:
        table = resource.create_table(
            TableName='repokid_roles',
            KeySchema=[
                {
                    'AttributeName': 'RoleId',
                    'KeyType': 'HASH'  # Partition key
                }
            ],
            AttributeDefinitions=[
                {
                    'AttributeName': 'RoleId',
                    'AttributeType': 'S'
                },
                {
                    'AttributeName': 'RoleName',
                    'AttributeType': 'S'
                },
                {
                    'AttributeName': 'Account',
                    'AttributeType': 'S'
                }
            ],
            ProvisionedThroughput={
                'ReadCapacityUnits': 50,
                'WriteCapacityUnits': 50
            },
            GlobalSecondaryIndexes=[
                {
                    'IndexName': 'Account',
                    'KeySchema': [
                        {
                            'AttributeName': 'Account',
                            'KeyType': 'HASH'
                        }
                    ],
                    'Projection': {
                        'ProjectionType': 'KEYS_ONLY',
                    },
                    'ProvisionedThroughput': {
                        'ReadCapacityUnits': 10,
                        'WriteCapacityUnits': 10
                    }
                },
                {
                    'IndexName': 'RoleName',
                    'KeySchema': [
                        {
                            'AttributeName': 'RoleName',
                            'KeyType': 'HASH'
                        }
                    ],
                    'Projection':
                    {
                        'ProjectionType': 'KEYS_ONLY',
                    },
                    'ProvisionedThroughput': {
                        'ReadCapacityUnits': 10,
                        'WriteCapacityUnits': 10
                    }
                }])

    except BotoClientError as e:
        LOGGER.error(e)
    return table
Beispiel #29
0
 def decorated_func(*args, **kwargs):
     try:
         return func(*args, **kwargs)
     except BotoClientError as e:
         LOGGER.error('Dynamo table error: {}'.format(e))
         sys.exit(1)
Beispiel #30
0
def _get_repoable_permissions(account_number, role_name, permissions, aa_data,
                              no_repo_permissions, minimum_age, hooks):
    """
    Generate a list of repoable permissions for a role based on the list of all permissions the role's policies
    currently allow and Access Advisor data for the services included in the role's policies.

    The first step is to come up with a list of services that were used within the time threshold (the same defined)
    in the age filter config. Permissions are repoable if they aren't in the used list, aren't in the constant list
    of unsupported services/actions (IAM_ACCESS_ADVISOR_UNSUPPORTED_SERVICES, IAM_ACCESS_ADVISOR_UNSUPPORTED_ACTIONS),
    and aren't being temporarily ignored because they're on the no_repo_permissions list (newly added).

    Args:
        account_number
        role_name
        permissions (list): The full list of permissions that the role's permissions allow
        aa_data (list): A list of Access Advisor data for a role. Each element is a dictionary with a couple required
                        attributes: lastAuthenticated (epoch time in milliseconds when the service was last used and
                        serviceNamespace (the service used)
        no_repo_permissions (dict): Keys are the name of permissions and values are the time the entry expires
        minimum_age: Minimum age of a role (in days) for it to be repoable
        hooks: Dict containing hook names and functions to run

    Returns:
        set: Permissions that are 'repoable' (not used within the time threshold)
    """
    ago = datetime.timedelta(minimum_age)
    now = datetime.datetime.now(tzlocal())

    current_time = time.time()
    no_repo_list = [
        perm.lower() for perm in no_repo_permissions
        if no_repo_permissions[perm] > current_time
    ]

    # cast all permissions to lowercase
    permissions = [permission.lower() for permission in permissions]
    potentially_repoable_permissions = {
        permission: RepoablePermissionDecision()
        for permission in permissions if permission not in no_repo_list
    }

    used_services = set()
    for service in aa_data:
        (accessed, valid_authenticated) = _get_epoch_authenticated(
            service['lastAuthenticated'])

        if not accessed:
            continue

        if not valid_authenticated:
            LOGGER.error(
                "Got malformed Access Advisor data for {role_name} in {account_number} for service {service}"
                ": {last_authenticated}".format(
                    role_name=role_name,
                    account_number=account_number,
                    service=service.get('serviceNamespace'),
                    last_authenticated=service['lastAuthenticated']))
            used_services.add(service['serviceNamespace'])

        accessed = datetime.datetime.fromtimestamp(accessed, tzlocal())
        if accessed > now - ago:
            used_services.add(service['serviceNamespace'])

    for permission_name, permission_decision in potentially_repoable_permissions.items(
    ):
        if permission_name.split(
                ':')[0] in IAM_ACCESS_ADVISOR_UNSUPPORTED_SERVICES:
            LOGGER.warn('skipping {}'.format(permission_name))
            continue

        # we have an unused service but need to make sure it's repoable
        if permission_name.split(':')[0] not in used_services:
            if permission_name in IAM_ACCESS_ADVISOR_UNSUPPORTED_ACTIONS:
                LOGGER.warn('skipping {}'.format(permission_name))
                continue

            permission_decision.repoable = True
            permission_decision.decider = 'Access Advisor'

    hooks_output = repokid.hooks.call_hooks(
        hooks, 'DURING_REPOABLE_CALCULATION', {
            'account_number': account_number,
            'role_name': role_name,
            'potentially_repoable_permissions':
            potentially_repoable_permissions,
            'minimum_age': minimum_age
        })

    LOGGER.debug(
        'Repoable permissions for role {role_name} in {account_number}:\n{repoable}'
        .format(role_name=role_name,
                account_number=account_number,
                repoable=''.join(
                    '{}: {}\n'.format(perm, decision.decider)
                    for perm, decision in
                    hooks_output['potentially_repoable_permissions'].items())))

    return set([
        permission_name for permission_name, permission_value in
        hooks_output['potentially_repoable_permissions'].items()
        if permission_value.repoable
    ])