Ejemplo n.º 1
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))
Ejemplo n.º 2
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(
            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))
Ejemplo n.º 3
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
Ejemplo n.º 4
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)

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

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

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

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

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

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

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

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

    policies_length = len(json.dumps(repoed_policies))

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

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

    conn = config['connection_iam']
    conn['account_number'] = account_number

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

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

        for policy_name, policy in repoed_policies.items():
            try:
                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:
                error = 'Exception calling PutRolePolicy on {role}/{policy} in account {account}\n{e}\n'.format(
                    role=role.role_name, policy=policy_name, account=account_number, e=str(e))
                LOGGER.error(error)
                errors.append(error)

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

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

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

    if not errors:
        # repos will stay scheduled until they are successful
        set_role_data(dynamo_table, role.role_id, {'Repoed': datetime.datetime.utcnow().isoformat()})
        _update_repoed_description(role.role_name, **conn)
        _update_role_data(role, dynamo_table, account_number, config, conn, hooks, source='Repo', add_no_repo=False)
        LOGGER.info('Successfully repoed role: {} in account {}'.format(role.role_name, account_number))
    return errors
Ejemplo n.º 5
0
def display_role(account_number, role_name, dynamo_table, config, hooks):
    """
    Displays data about a role in a given account:
      1) Name, which filters are disqualifying it from repo, if it's repoable, total/repoable permissions,
         when it was last repoed, which services can be repoed
      2) The policy history: how discovered (repo, scan, etc), the length of the policy, and start of the contents
      3) Captured stats entry for the role
      4) A list of all services/actions currently allowed and whether they are repoable
      5) What the new policy would look like after repoing (if it is repoable)

    Args:
        account_number (string)
        role_name (string)

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

    role = Role(get_role_data(dynamo_table, role_id))

    print "\n\nRole repo data:"
    headers = ['Name', 'Refreshed', 'Disqualified By', 'Can be repoed', 'Permissions', 'Repoable', 'Repoed', 'Services']
    rows = [[role.role_name,
             role.refreshed,
             role.disqualified_by,
             len(role.disqualified_by) == 0,
             role.total_permissions,
             role.repoable_permissions,
             role.repoed,
             role.repoable_services]]
    print tabulate(rows, headers=headers) + '\n\n'

    print "Policy history:"
    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) + '\n\n'

    print "Stats:"
    headers = ['Date', 'Event Type', 'Permissions Count', 'Disqualified By']
    rows = []
    for stats_entry in role.stats:
        rows.append([stats_entry['Date'],
                     stats_entry['Source'],
                     stats_entry['PermissionsCount'],
                     stats_entry.get('DisqualifiedBy', [])])
    print tabulate(rows, headers=headers) + '\n\n'

    # can't do anymore if we don't have AA data
    if not role.aa_data:
        LOGGER.warn('ARN not found in Access Advisor: {}'.format(role.arn))
        return

    warn_unknown_permissions = config.get('warnings', {}).get('unknown_permissions', False)
    repoable_permissions = set([])

    permissions = roledata._get_role_permissions(role, warn_unknown_perms=warn_unknown_permissions)
    if len(role.disqualified_by) == 0:
        repoable_permissions = roledata._get_repoable_permissions(account_number, role.role_name, permissions,
                                                                  role.aa_data, role.no_repo_permissions,
                                                                  config['filter_config']['AgeFilter']['minimum_age'],
                                                                  hooks)

    print "Repoable services and permissions"
    headers = ['Service', 'Action', 'Repoable']
    rows = []
    for permission in permissions:
        service = permission.split(':')[0]
        action = permission.split(':')[1]
        repoable = permission in repoable_permissions
        rows.append([service, action, repoable])

    rows = sorted(rows, key=lambda x: (x[2], x[0], x[1]))
    print tabulate(rows, headers=headers) + '\n\n'

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

    if repoed_policies:
        print('Repo\'d Policies: \n{}'.format(json.dumps(repoed_policies, indent=2, sort_keys=True)))
    else:
        print('All Policies Removed')

    # need to check if all policies would be too large
    if len(json.dumps(repoed_policies)) > MAX_AWS_POLICY_SIZE:
        LOGGER.warning("Policies would exceed the AWS size limit after repo for role: {}.  "
                       "Please manually minify.".format(role_name))
Ejemplo n.º 6
0
def _display_role(account_number, role_name, dynamo_table, config, hooks):
    """
    Displays data about a role in a given account:
      1) Name, which filters are disqualifying it from repo, if it's repoable, total/repoable permissions,
         when it was last repoed, which services can be repoed
      2) The policy history: how discovered (repo, scan, etc), the length of the policy, and start of the contents
      3) Captured stats entry for the role
      4) A list of all services/actions currently allowed and whether they are repoable
      5) What the new policy would look like after repoing (if it is repoable)

    Args:
        account_number (string)
        role_name (string)

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

    role = Role(get_role_data(dynamo_table, role_id))

    print("\n\nRole repo data:")
    headers = [
        "Name",
        "Refreshed",
        "Disqualified By",
        "Can be repoed",
        "Permissions",
        "Repoable",
        "Repoed",
        "Services",
    ]
    rows = [
        [
            role.role_name,
            role.refreshed,
            role.disqualified_by,
            len(role.disqualified_by) == 0,
            role.total_permissions,
            role.repoable_permissions,
            role.repoed,
            role.repoable_services,
        ]
    ]
    print(tabulate(rows, headers=headers) + "\n\n")

    print("Policy history:")
    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) + "\n\n")

    print("Stats:")
    headers = ["Date", "Event Type", "Permissions Count", "Disqualified By"]
    rows = []
    for stats_entry in role.stats:
        rows.append(
            [
                stats_entry["Date"],
                stats_entry["Source"],
                stats_entry["PermissionsCount"],
                stats_entry.get("DisqualifiedBy", []),
            ]
        )
    print(tabulate(rows, headers=headers) + "\n\n")

    # can't do anymore if we don't have AA data
    if not role.aa_data:
        LOGGER.warn("ARN not found in Access Advisor: {}".format(role.arn))
        return

    warn_unknown_permissions = config.get("warnings", {}).get(
        "unknown_permissions", False
    )
    repoable_permissions = set([])

    permissions, eligible_permissions = roledata._get_role_permissions(
        role, warn_unknown_perms=warn_unknown_permissions
    )
    if len(role.disqualified_by) == 0:
        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,
        )

    print("Repoable services and permissions")
    headers = ["Service", "Action", "Repoable"]
    rows = []
    for permission in permissions:
        service = permission.split(":")[0]
        action = permission.split(":")[1]
        repoable = permission in repoable_permissions
        rows.append([service, action, repoable])

    rows = sorted(rows, key=lambda x: (x[2], x[0], x[1]))
    print(tabulate(rows, headers=headers) + "\n\n")

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

    if repoed_policies:
        print(
            "Repo'd Policies: \n{}".format(
                json.dumps(repoed_policies, indent=2, sort_keys=True)
            )
        )
    else:
        print("All Policies Removed")

    # need to check if all policies would be too large
    if inline_policies_size_exceeds_maximum(repoed_policies):
        LOGGER.warning(
            "Policies would exceed the AWS size limit after repo for role: {}.  "
            "Please manually minify.".format(role_name)
        )
Ejemplo n.º 7
0
def _deal_with_policies(role, account_number, config, hooks, scheduled,
                        role_name, dynamo_table, commit, continuing):
    errors = []
    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