Beispiel #1
0
def update_role_data(dynamo_table,
                     account_number,
                     role,
                     current_policy,
                     source='Scan',
                     add_no_repo=True):
    """
    Compare the current version of a policy for a role and what has been previously stored in Dynamo.
      - If current and new policy versions are different store the new version in Dynamo. Add any newly added
          permissions to temporary permission blacklist. Purge any old entries from permission blacklist.
      - Refresh the updated time on the role policy
      - If the role is completely new, store the first version in Dynamo
      - Updates the role with full history of policies, including current version

    Args:
        dynamo_table
        account_number
        role (Role): current role being updated
        current_policy (dict): representation of the current policy version
        source: Default 'Scan' but could be Repo, Rollback, etc

    Returns:
        None
    """

    # policy_entry: source, discovered, policy
    stored_role = get_role_data(dynamo_table,
                                role.role_id,
                                fields=['OptOut', 'Policies'])
    if not stored_role:
        role_dict = store_initial_role_data(dynamo_table, role.arn,
                                            role.create_date, role.role_id,
                                            role.role_name, account_number,
                                            current_policy)
        role.set_attributes(role_dict)
        LOGGER.info('Added new role ({}): {}'.format(role.role_id, role.arn))
    else:
        # is the policy list the same as the last we had?
        old_policy = stored_role['Policies'][-1]['Policy']
        if current_policy != old_policy:
            add_new_policy_version(dynamo_table, role, current_policy, source)
            LOGGER.info(
                '{} has different inline policies than last time, adding to role store'
                .format(role.arn))

            newly_added_permissions = find_newly_added_permissions(
                old_policy, current_policy)
        else:
            newly_added_permissions = set()

        if add_no_repo:
            update_no_repo_permissions(dynamo_table, role,
                                       newly_added_permissions)
        update_opt_out(dynamo_table, role)
        set_role_data(dynamo_table, role.role_id,
                      {'Refreshed': datetime.datetime.utcnow().isoformat()})

        role.policies = get_role_data(dynamo_table,
                                      role.role_id,
                                      fields=['Policies'])['Policies']
Beispiel #2
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 #3
0
def _display_roles(account_number, dynamo_table, inactive=False):
    """
    Display a table with data about all roles in an account and write a csv file with the data.

    Args:
        account_number (string)
        inactive (bool): show roles that have historically (but not currently) existed in the account if True

    Returns:
        None
    """

    headers = [
        "Name",
        "Refreshed",
        "Disqualified By",
        "Can be repoed",
        "Permissions",
        "Policies Repoable",
        "Services",
        "Repoed",
        "Managed Permissions",
        "Managed Policies Repoable"
        "Managed Services",
    ]

    rows = list()

    roles = Roles([
        Role.parse_obj(get_role_data(dynamo_table, roleID))
        for roleID in tqdm(role_ids_for_account(dynamo_table, account_number))
    ])

    if not inactive:
        roles = roles.filter(active=True)

    for role in roles:
        rows.append([
            role.role_name,
            role.refreshed,
            role.disqualified_by,
            len(role.disqualified_by) == 0,
            role.total_permissions,
            role.repoable_permissions,
            role.repoable_services,
            role.repoed,
            role.total_managed_permissions,
            role.repoable_managed_permissions,
            role.repoable_managed_services,
        ])

    rows = sorted(rows, key=lambda x: (x[5], x[0], x[4]))
    rows.insert(0, headers)
    # print tabulate(rows, headers=headers)
    t.view(rows)
    with open("table.csv", "w") as csvfile:
        csv_writer = csv.writer(csvfile)
        csv_writer.writerow(headers)
        for row in rows:
            csv_writer.writerow(row)
Beispiel #4
0
def list_repoable_services(dynamo_table, message):
    role_id = dynamo.find_role_in_cache(dynamo_table, message.account,
                                        message.role_name)

    if not role_id:
        return ResponderReturn(
            successful=False,
            return_message='Unable to find role {} in account {}'.format(
                message.role_name, message.account))
    else:
        role_data = dynamo.get_role_data(dynamo_table,
                                         role_id,
                                         fields=['RepoableServices'])

        (repoable_permissions, repoable_services
         ) = roledata._convert_repoed_service_to_sorted_perms_and_services(
             role_data['RepoableServices'])

        repoable_services = role_data['RepoableServices']
        return ResponderReturn(
            successful=True,
            return_message=(
                'Role {} in account {} has:\n    Repoable Services: \n{}'
                '\n\n    Repoable Permissions: \n{}'.format(
                    message.role_name, message.account,
                    '\n'.join([service for service in repoable_services]),
                    '\n'.join([perm for perm in repoable_permissions]))))
Beispiel #5
0
def show_scheduled_roles(account_number, dynamo_table):
    """
    Show scheduled repos for a given account.  For each scheduled show whether scheduled time is elapsed or not.
    """
    roles = Roles([
        Role(get_role_data(dynamo_table, roleID))
        for roleID in tqdm(role_ids_for_account(dynamo_table, account_number))
    ])

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

    header = ["Role name", "Scheduled", "Scheduled Time Elapsed?"]
    rows = []

    curtime = int(time.time())

    for role in roles:
        rows.append([
            role.role_name,
            dt.fromtimestamp(role.repo_scheduled).strftime("%Y-%m-%d %H:%M"),
            role.repo_scheduled < curtime,
        ])

    print(tabulate(rows, headers=header))
Beispiel #6
0
def update_no_repo_permissions(dynamo_table, 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(
        dynamo_table, 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
    set_role_data(dynamo_table, role.role_id,
                  {'NoRepoPermissions': role.no_repo_permissions})
Beispiel #7
0
def remove_opt_out(dynamo_table, message):
    role_id = dynamo.find_role_in_cache(dynamo_table, message.account,
                                        message.role_name)

    if not role_id:
        return ResponderReturn(
            successful=False,
            return_message="Unable to find role {} in account {}".format(
                message.role_name, message.account),
        )

    role_data = dynamo.get_role_data(dynamo_table, role_id, fields=["OptOut"])

    if "OptOut" not in role_data or not role_data["OptOut"]:
        return ResponderReturn(
            successful=False,
            return_message="Role {} in account {} wasn't opted out".format(
                message.role_name, message.account),
        )
    else:
        dynamo.set_role_data(dynamo_table, role_id, {"OptOut": {}})
        return ResponderReturn(
            successful=True,
            return_message="Cancelled opt-out for role {} in account {}".
            format(message.role_name, message.account),
        )
Beispiel #8
0
def update_stats(dynamo_table, 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:
        new_stats = {
            'Date': datetime.datetime.utcnow().isoformat(),
            'DisqualifiedBy': role.disqualified_by,
            'PermissionsCount': role.total_permissions,
            'Source': source
        }
        cur_stats = get_role_data(dynamo_table, role.role_id,
                                  fields=['Stats'])['Stats'][-1]

        for item in ['DisqualifiedBy', 'PermissionsCount']:
            if new_stats[item] != cur_stats[item]:
                add_to_end_of_list(dynamo_table, role.role_id, 'Stats',
                                   new_stats)
Beispiel #9
0
def add_new_managed_policy_version(dynamo_table, role, current_managed_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_managed_policy (dict)
        update_source (string): ['Repo', 'Scan', 'Restore']

    Returns:
        None
    """
    policy_entry = {
        "Source": update_source,
        "Discovered": datetime.datetime.utcnow().isoformat(),
        "Policy": current_managed_policy,
    }

    add_to_end_of_list(dynamo_table, role.role_id, "ManagedPolicies",
                       policy_entry)
    role.managed_policies = get_role_data(dynamo_table,
                                          role.role_id,
                                          fields=["ManagedPolicies"
                                                  ])["ManagedPolicies"]
Beispiel #10
0
def _find_roles_with_permissions(permissions, dynamo_table, output_file):
    """
    Search roles in all accounts for a policy with any of the provided permissions, log the ARN of each role.

    Args:
        permissions (list[string]): The name of the permissions to find
        output_file (string): filename to write the output

    Returns:
        None
    """
    arns = list()
    for roleID in role_ids_for_all_accounts(dynamo_table):
        role = Role.parse_obj(
            get_role_data(dynamo_table,
                          roleID,
                          fields=["Policies", "RoleName", "Arn", "Active"]))
        role_permissions, _ = roledata.get_role_permissions(role)

        permissions = set([p.lower() for p in permissions])
        found_permissions = permissions.intersection(role_permissions)

        if found_permissions and role.active:
            arns.append(role.arn)
            LOGGER.info("ARN {arn} has {permissions}".format(
                arn=role.arn, permissions=list(found_permissions)))

    if not output_file:
        return

    with open(output_file, "w") as fd:
        json.dump(arns, fd)

    LOGGER.info('Ouput written to file "{output_file}"'.format(
        output_file=output_file))
Beispiel #11
0
def list_role_rollbacks(dynamo_table, message):
    role_id = dynamo.find_role_in_cache(dynamo_table, message.account,
                                        message.role_name)

    if not role_id:
        return ResponderReturn(
            successful=False,
            return_message="Unable to find role {} in account {}".format(
                message.role_name, message.account),
        )
    else:
        role_data = dynamo.get_role_data(dynamo_table,
                                         role_id,
                                         fields=["Policies"])
        return_val = "Restorable versions for role {} in account {}\n".format(
            message.role_name, message.account)
        for index, policy_version in enumerate(role_data["Policies"]):
            total_permissions, _ = roledata._get_permissions_in_policy(
                policy_version["Policy"])
            return_val += "({:>3}):  {:<5}     {:<15}  {}\n".format(
                index,
                len(total_permissions),
                policy_version["Discovered"],
                policy_version["Source"],
            )
        return ResponderReturn(successful=True, return_message=return_val)
Beispiel #12
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 #13
0
def schedule_repo(account_number, dynamo_table, config, hooks):
    """
    Schedule a repo for a given account.  Schedule repo for a time in the future (default 7 days) for any roles in
    the account with repoable permissions.
    """
    scheduled_roles = []

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

    scheduled_time = int(time.time()) + (86400 * config.get('repo_schedule_period_days', 7))
    for role in roles:
        if role.repoable_permissions > 0 and not role.repo_scheduled:
            role.repo_scheduled = scheduled_time
            # freeze the scheduled perms to whatever is repoable right now
            set_role_data(dynamo_table, role.role_id,
                          {'RepoScheduled': scheduled_time, 'ScheduledPerms': role.repoable_services})

            scheduled_roles.append(role)

    LOGGER.info("Scheduled repo for {} days from now for account {} and these roles:\n\t{}".format(
                config.get('repo_schedule_period_days', 7),
                account_number,
                ', '.join([r.role_name for r in scheduled_roles])))

    repokid.hooks.call_hooks(hooks, 'AFTER_SCHEDULE_REPO', {'roles': scheduled_roles})
Beispiel #14
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 #15
0
def opt_out(dynamo_table, message):
    if CONFIG:
        opt_out_period = CONFIG.get("opt_out_period_days", 90)
    else:
        opt_out_period = 90

    if not message.reason or not message.requestor:
        return ResponderReturn(
            successful=False,
            return_message="Reason and requestor must be specified")

    role_id = dynamo.find_role_in_cache(dynamo_table, message.account,
                                        message.role_name)

    if not role_id:
        return ResponderReturn(
            successful=False,
            return_message="Unable to find role {} in account {}".format(
                message.role_name, message.account),
        )

    role_data = dynamo.get_role_data(dynamo_table, role_id, fields=["OptOut"])
    if "OptOut" in role_data and role_data["OptOut"]:

        timestr = time.strftime("%m/%d/%y",
                                time.localtime(role_data["OptOut"]["expire"]))
        return ResponderReturn(
            successful=False,
            return_message=
            ("Role {} in account {} is already opted out by {} for reason {} "
             "until {}".format(
                 message.role_name,
                 message.account,
                 role_data["OptOut"]["owner"],
                 role_data["OptOut"]["reason"],
                 timestr,
             )),
        )

    else:
        current_dt = datetime.datetime.fromtimestamp(time.time())
        expire_dt = current_dt + datetime.timedelta(opt_out_period)
        expire_epoch = int(
            (expire_dt - datetime.datetime(1970, 1, 1)).total_seconds())
        new_opt_out = {
            "owner": message.requestor,
            "reason": message.reason,
            "expire": expire_epoch,
        }
        dynamo.set_role_data(dynamo_table, role_id, {"OptOut": new_opt_out})
        return ResponderReturn(
            successful=True,
            return_message="Role {} in account {} opted-out until {}".format(
                message.role_name, message.account,
                expire_dt.strftime("%m/%d/%y")),
        )
Beispiel #16
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, "w") 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),
                     exc_info=True)
    else:
        LOGGER.info("Successfully wrote stats to {}".format(output_file))
Beispiel #17
0
def _schedule_repo(account_number, dynamo_table, config, hooks):
    """
    Schedule a repo for a given account.  Schedule repo for a time in the future (default 7 days) for any roles in
    the account with repoable permissions.
    """
    scheduled_roles = []

    roles = Roles([
        Role.parse_obj(get_role_data(dynamo_table, roleID))
        for roleID in tqdm(role_ids_for_account(dynamo_table, account_number))
    ])

    scheduled_time = int(
        time.time()) + (86400 * config.get("repo_schedule_period_days", 7))
    for role in roles:
        if not role.aa_data:
            LOGGER.warning("Not scheduling %s; missing Access Advisor data",
                           role.arn)
            continue
        if not role.repoable_permissions > 0:
            LOGGER.debug("Not scheduling %s; no repoable permissions",
                         role.arn)
            continue
        if role.repo_scheduled:
            LOGGER.debug(
                "Not scheduling %s; already scheduled for %s",
                role.arn,
                role.repo_scheduled,
            )
            continue

        role.repo_scheduled = scheduled_time
        # freeze the scheduled perms to whatever is repoable right now
        set_role_data(
            dynamo_table,
            role.role_id,
            {
                "RepoScheduled": scheduled_time,
                "ScheduledPerms": role.repoable_services,
            },
        )

        scheduled_roles.append(role)

    LOGGER.info(
        "Scheduled repo for {} days from now for account {} and these roles:\n\t{}"
        .format(
            config.get("repo_schedule_period_days", 7),
            account_number,
            ", ".join([r.role_name for r in scheduled_roles]),
        ))

    repokid.hooks.call_hooks(hooks, "AFTER_SCHEDULE_REPO",
                             {"roles": scheduled_roles})
Beispiel #18
0
def opt_out(dynamo_table, message):
    if CONFIG:
        opt_out_period = CONFIG.get('opt_out_period_days', 90)
    else:
        opt_out_period = 90

    if not message.reason or not message.requestor:
        return ResponderReturn(
            successful=False,
            return_message='Reason and requestor must be specified')

    role_id = dynamo.find_role_in_cache(dynamo_table, message.account,
                                        message.role_name)

    if not role_id:
        return ResponderReturn(
            successful=False,
            return_message='Unable to find role {} in account {}'.format(
                message.role_name, message.account))

    role_data = dynamo.get_role_data(dynamo_table, role_id, fields=['OptOut'])
    if 'OptOut' in role_data and role_data['OptOut']:

        timestr = time.strftime('%m/%d/%y',
                                time.localtime(role_data['OptOut']['expire']))
        return ResponderReturn(
            successful=False,
            return_message=
            ('Role {} in account {} is already opted out by {} for reason {} '
             'until {}'.format(message.role_name, message.account,
                               role_data['OptOut']['owner'],
                               role_data['OptOut']['reason'], timestr)))

    else:
        current_dt = datetime.datetime.fromtimestamp(time.time())
        expire_dt = current_dt + datetime.timedelta(opt_out_period)
        expire_epoch = int(
            (expire_dt - datetime.datetime(1970, 1, 1)).total_seconds())
        new_opt_out = {
            'owner': message.requestor,
            'reason': message.reason,
            'expire': expire_epoch
        }
        dynamo.set_role_data(dynamo_table, role_id, {'OptOut': new_opt_out})
        return ResponderReturn(
            successful=True,
            return_message='Role {} in account {} opted-out until {}'.format(
                message.role_name, message.account,
                expire_dt.strftime('%m/%d/%y')))
Beispiel #19
0
def find_roles_with_permission(permission, dynamo_table):
    """
    Search roles in all accounts for a policy with a given permission, log the ARN of each role with this permission

    Args:
        permission (string): The name of the permission to find

    Returns:
        None
    """
    for roleID in role_ids_for_all_accounts(dynamo_table):
        role = Role(get_role_data(dynamo_table, roleID, fields=['Policies', 'RoleName', 'Arn', 'Active']))
        permissions = roledata._get_role_permissions(role)
        if permission.lower() in permissions and role.active:
            LOGGER.info('ARN {arn} has {permission}'.format(arn=role.arn, permission=permission))
Beispiel #20
0
def list_repoable_services(dynamo_table, message):
    role_id = dynamo.find_role_in_cache(dynamo_table, message.account,
                                        message.role_name)

    if not role_id:
        return ResponderReturn(
            successful=False,
            return_message='Unable to find role {} in account {}'.format(
                message.role_name, message.account))
    else:
        role_data = dynamo.get_role_data(dynamo_table,
                                         role_id,
                                         fields=['RepoableServices'])
        repoable_services = role_data['RepoableServices']
        return ResponderReturn(
            successful=True,
            return_message='Repoable services from role {} in account {}: {}'.
            format(message.role_name, message.account, repoable_services))
Beispiel #21
0
def cancel_scheduled_repo(account_number, role_name, dynamo_table):
    """
    Cancel scheduled repo for a role in an account
    """
    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})
    LOGGER.info('Successfully cancelled scheduled repo for role {} in account {}'.format(role.role_name,
                role.account))
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.parse_obj(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 list_role_rollbacks(dynamo_table, message):
    role_id = dynamo.find_role_in_cache(dynamo_table, message.account,
                                        message.role_name)

    if not role_id:
        return ResponderReturn(
            successful=False,
            return_message='Unable to find role {} in account {}'.format(
                message.role_name, message.account))
    else:
        role_data = dynamo.get_role_data(dynamo_table,
                                         role_id,
                                         fields=['Policies'])
        return_val = 'Restorable versions for role {} in account {}\n'.format(
            message.role_name, message.account)
        for index, policy_version in enumerate(role_data['Policies']):
            return_val += '({:>3}):  {:<5}     {:<15}  {}\n'.format(
                index, len(str(policy_version['Policy'])),
                policy_version['Discovered'], policy_version['Source'])
        return ResponderReturn(successful=True, return_message=return_val)
Beispiel #24
0
def find_and_mark_inactive(dynamo_table, 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(dynamo_table, account_number))
    inactive_roles = known_roles - active_roles

    for roleID in inactive_roles:
        role_dict = get_role_data(dynamo_table, roleID, fields=['Active', 'Arn'])
        if role_dict.get('Active'):
            set_role_data(dynamo_table, roleID, {'Active': False})
Beispiel #25
0
def remove_opt_out(dynamo_table, message):
    role_id = dynamo.find_role_in_cache(dynamo_table, message.account,
                                        message.role_name)

    if not role_id:
        return ResponderReturn(
            successful=False,
            return_message='Unable to find role {} in account {}'.format(
                message.role_name, message.account))

    role_data = dynamo.get_role_data(dynamo_table, role_id, fields=['OptOut'])

    if 'OptOut' not in role_data or not role_data['OptOut']:
        return ResponderReturn(
            successful=False,
            return_message='Role {} in account {} wasn\'t opted out'.format(
                message.role_name, message.account))
    else:
        dynamo.set_role_data(dynamo_table, role_id, {'OptOut': {}})
        return ResponderReturn(
            successful=True,
            return_message='Cancelled opt-out for role {} in account {}'.
            format(message.role_name, message.account))
Beispiel #26
0
def schedule_repo(account_number, dynamo_table, config):
    """
    Schedule a repo for a given account.  Schedule repo for a time in the future (default 7 days) for any roles in
    the account with repoable permissions.
    """
    scheduled_roles = []

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

    scheduled_time = int(
        time.time()) + (86400 * config.get('repo_schedule_period_days', 7))
    for role in roles:
        if role.repoable_permissions > 0:
            set_role_data(dynamo_table, role.role_id,
                          {'RepoScheduled': scheduled_time})
            scheduled_roles.append(role.role_name)

    LOGGER.info(
        "Scheduled repo for {} days from now for these roles:\n\t{}".format(
            config.get('repo_schedule_period_days', 7),
            ', '.join([r for r in scheduled_roles])))
Beispiel #27
0
def _repo_all_roles(account_number,
                    dynamo_table,
                    config,
                    hooks,
                    commit=False,
                    scheduled=True,
                    limit=-1):
    """
    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
        limit (int): limit number of roles to be repoed per run (< 0 is unlimited)

    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]),
    ))

    repokid.hooks.call_hooks(hooks, "BEFORE_REPO_ROLES", {
        "account_number": account_number,
        "roles": roles
    })

    count = 0
    repoed = Roles([])
    for role in roles:
        if limit >= 0 and count == limit:
            break
        error = _repo_role(
            account_number,
            role.role_name,
            dynamo_table,
            config,
            hooks,
            commit=commit,
            scheduled=scheduled,
        )
        if error:
            errors.append(error)
        repoed.append(role)
        count += 1

    if errors:
        LOGGER.error(
            f"Error(s) during repo: \n{errors} (account: {account_number})")
    else:
        LOGGER.info(
            f"Successfully repoed {count} roles in account {account_number}")

    repokid.hooks.call_hooks(
        hooks,
        "AFTER_REPO_ROLES",
        {
            "account_number": account_number,
            "roles": repoed,
            "errors": errors
        },
    )
Beispiel #28
0
def _repo_role(
    account_number,
    role_name,
    dynamo_table,
    config,
    hooks,
    commit=False,
    scheduled=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)

    continuing = True

    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))
        continuing = False

    if not role.aa_data:
        LOGGER.warning("ARN not found in Access Advisor: {}".format(role.arn))
        continuing = False

    if not role.repoable_permissions:
        LOGGER.info("No permissions to repo for role {} in account {}".format(
            role_name, account_number))
        continuing = False

    # 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),
            exc_info=True,
        )
        continuing = False

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

    # if this is a scheduled repo we need to filter out permissions that weren't previously scheduled
    if scheduled:
        repoable_permissions = roledata._filter_scheduled_repoable_perms(
            repoable_permissions, role.scheduled_perms)

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

    if inline_policies_size_exceeds_maximum(repoed_policies):
        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)
        continuing = False

    # if we aren't repoing for some reason, unschedule the role
    if not continuing:
        set_role_data(dynamo_table, role.role_id, {
            "RepoScheduled": 0,
            "ScheduledPerms": []
        })
        return

    if not commit:
        log_deleted_and_repoed_policies(deleted_policy_names, repoed_policies,
                                        role_name, account_number)
        return

    conn = config["connection_iam"]
    conn["account_number"] = account_number

    for name in deleted_policy_names:
        error = delete_policy(name, role, account_number, conn)
        if error:
            LOGGER.error(error)
            errors.append(error)

    if repoed_policies:
        error = replace_policies(repoed_policies, role, account_number, conn)
        if error:
            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,
        "ScheduledPerms": []
    })

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

    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)
        partial_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 #29
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

    conn = config["connection_iam"]
    conn["account_number"] = account_number

    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(
                f"Pushing cached policy: {policy_name} (role: {role.role_name} account {account_number})"
            )

            put_role_policy(
                RoleName=role.role_name,
                PolicyName=policy_name,
                PolicyDocument=json.dumps(policy, indent=2, sort_keys=True),
                **conn,
            )

        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, exc_info=True)
            errors.append(message)

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

    if policies_to_remove:
        for policy_name in policies_to_remove:
            try:
                LOGGER.info(
                    f"Deleting policy {policy_name} for rollback (role: {role.role_name} account {account_number})"
                )
                delete_role_policy(RoleName=role.role_name,
                                   PolicyName=policy_name,
                                   **conn)

            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, exc_info=True)
                errors.append(message)

    partial_update_role_data(
        role,
        dynamo_table,
        account_number,
        config,
        conn,
        hooks,
        source="Restore",
        add_no_repo=False,
    )

    if not errors:
        LOGGER.info(
            f"Successfully restored selected version {selection} of role policies (role: {role.role_name} "
            f"account: {account_number}")
    return errors
Beispiel #30
0
def update_role_data(dynamo_table,
                     account_number,
                     role,
                     current_policy,
                     current_managed_policy,
                     source="Scan",
                     add_no_repo=True,
                     include_managed_policies=True):
    """
    Compare the current version of a policy for a role and what has been previously stored in Dynamo.
      - If current and new policy versions are different store the new version in Dynamo. Add any newly added
          permissions to temporary permission blocklist. Purge any old entries from permission blocklist.
      - Refresh the updated time on the role policy
      - If the role is completely new, store the first version in Dynamo
      - Updates the role with full history of policies, including current version

    Args:
        dynamo_table
        account_number
        role (Role): current role being updated
        current_policy (dict): representation of the current policy version
        current_managed_policy (dict): representation of the current managed policy versions
        source: Default 'Scan' but could be Repo, Rollback, etc
        add_no_repo (bool)

    Returns:
        None
    """

    # policy_entry: source, discovered, policy
    stored_role = get_role_data(
        dynamo_table,
        role.role_id,
        fields=["OptOut", "Policies", "ManagedPolicies", "Tags"])
    if not stored_role:
        role_dict = store_initial_role_data(
            dynamo_table,
            role.arn,
            role.create_date,
            role.role_id,
            role.role_name,
            account_number,
            current_policy,
            current_managed_policy,
            role.tags,
        )
        role_updates = Role.parse_obj(role_dict)
        update_dict = role_updates.dict(exclude_unset=True)
        role = role.copy(update=update_dict)
        LOGGER.info("Added new role ({}): {}".format(role.role_id, role.arn))
        return role
    else:
        # is the policy list the same as the last we had?
        old_policy = stored_role["Policies"][-1]["Policy"]
        if current_policy != old_policy:
            add_new_policy_version(dynamo_table, role, current_policy, source)
            LOGGER.info(
                "{} has different inline policies than last time, adding to role store"
                .format(role.arn))
            newly_added_permissions = find_newly_added_permissions(
                old_policy, current_policy)

        else:
            newly_added_permissions = set()

        # TODO Make this part of set_role_data instead to allow updating existing dynamo tables
        # TODO this code will not work with existing dynamo tables - because old roles won't have ManagedPolicies
        old_managed_policy = stored_role["ManagedPolicies"][-1]["Policy"]
        if current_managed_policy != old_managed_policy:
            add_new_managed_policy_version(dynamo_table, role,
                                           current_managed_policy, source)
            LOGGER.info(
                "{} has different managed policies than last time, adding to role store"
                .format(role.arn))

            newly_added_managed_permissions = find_newly_added_permissions(
                old_managed_policy, current_managed_policy)

        else:
            newly_added_managed_permissions = set()

        # update tags if needed
        if role.tags != stored_role.get("Tags", []):
            set_role_data(dynamo_table, role.role_id, {"Tags": role.tags})

        if add_no_repo:
            update_no_repo_permissions(dynamo_table, role,
                                       newly_added_permissions)
            if include_managed_policies:
                update_no_repo_permissions(dynamo_table, role,
                                           newly_added_managed_permissions)
        update_opt_out(dynamo_table, role)
        set_role_data(
            dynamo_table,
            role.role_id,
            {"Refreshed": datetime.datetime.utcnow().isoformat()},
        )

        # Update all data from Dynamo except CreateDate (it's in the wrong format) and DQ_by (we're going to recalc)
        current_role_data = get_role_data(dynamo_table, role.role_id)
        current_role_data.pop("CreateDate", None)
        current_role_data.pop("DisqualifiedBy", None)

        # Create an updated Role model to be returned to the caller
        role_updates = Role.parse_obj(current_role_data)
        update_dict = role_updates.dict(exclude_unset=True)
        role = role.copy(update=update_dict)

        return role