def execute(ctx): """Execute this module with the configured options""" if click.confirm( "[*] Do you want to attempt to harvest information on all network zones?", default=True): msg = "Attempting to harvest all network zones" LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") list_zones(ctx)
def delete_configuration_profile(ctx, config): msg = f'[*] Do you want to delete the configuration profile for {config["description"]} ({config["okta_url"]})?' if click.confirm(msg, default=True): file = Path(ctx.obj.config_dir / (config["id"] + ".json")) file.unlink() msg = f"Configuration file deleted ({file})" LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.secho(f"[*] {msg}", fg="green") delete_saved_data(ctx, config["id"])
def execute(ctx): """Execute this module with the configured options""" error = check_module_options(MODULE_OPTIONS) if error: return policy_id = MODULE_OPTIONS["policy_id"]["value"] rule_id = MODULE_OPTIONS["rule_id"]["value"] rule = get_policy_rule(ctx, policy_id, rule_id) if rule: if rule["status"] == "ACTIVE": click.echo("[*] Rule is ACTIVE") if click.confirm( f'[*] Do you want to deactivate rule {rule_id} ({rule["name"]})?', default=True): msg = f'Attempting to deactivate rule {rule_id} ({rule["name"]}) in policy {policy_id}' LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") set_policy_rule_state(ctx, policy_id, rule_id, operation="DEACTIVATE") elif rule["status"] == "INACTIVE": click.echo("[*] Rule is INACTIVE") if click.confirm( f'[*] Do you want to activate rule {rule_id} ({rule["name"]})?', default=True): msg = f'Attempting to activate rule {rule_id} ({rule["name"]}) in policy {policy_id}' LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") set_policy_rule_state(ctx, policy_id, rule_id, operation="ACTIVATE") else: click.echo(f'[*] Rule status is {rule["status"]}')
def execute(ctx): """Execute this module with the configured options""" if click.confirm( "[*] Do you want to attempt to harvest information on all groups? This may take a while to avoid " "exceeding API rate limits", default=True, ): msg = "Attempting to harvest all Okta groups" LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") list_groups(ctx)
def execute(ctx): """Execute this module with the configured options""" error = check_module_options(MODULE_OPTIONS) if error: return msg = f'Attempting to get profile and group memberships for user ID {MODULE_OPTIONS["id"]["value"]}' LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") get_user_object(ctx, MODULE_OPTIONS["id"]["value"]) get_user_groups(ctx, MODULE_OPTIONS["id"]["value"])
def execute(ctx): """Execute this module with the configured options""" error = check_module_options(MODULE_OPTIONS) if error: return msg = f'Attempting to reset MFA factors for user ID {MODULE_OPTIONS["id"]["value"]}' LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") url = f'{ctx.obj.base_url}/users/{MODULE_OPTIONS["id"]["value"]}/lifecycle/reset_factors' headers = { "Accept": "application/json", "Content-Type": "application/json", "Authorization": f"SSWS {ctx.obj.api_token}", } params = {} payload = {} try: response = ctx.obj.session.post(url, headers=headers, params=params, json=payload, timeout=7) except Exception as e: LOGGER.error(e, exc_info=True) index_event(ctx.obj.es, module=__name__, event_type="ERROR", event=e) click.secho(f"[!] {URL_OR_API_TOKEN_ERROR}", fg="red") response = None if response.ok: msg = f'MFA factors reset for user {MODULE_OPTIONS["id"]["value"]}' LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.secho(f"[*] {msg}", fg="green") get_user_object(ctx, MODULE_OPTIONS["id"]["value"]) else: msg = ( f"Error resetting MFA factors for Okta user\n" f" Response Code: {response.status_code} | Response Reason: {response.reason}\n" f' Error Code: {response.json().get("errorCode")} | Error Summary: {response.json().get("errorSummary")}' ) LOGGER.error(msg) index_event(ctx.obj.es, module=__name__, event_type="ERROR", event=msg) click.secho(f"[!] {msg}", fg="red") click.echo( "Check that the user's status is ACTIVE and that they have at least one factor enrolled" ) return
def execute(ctx): """Execute this module with the configured options""" if click.confirm( "[*] Do you want to attempt to harvest information for all Okta policies and policy rules?", default=True): harvested_policies = [] policy_types = ctx.obj.policy_types # Get a list of all policies by policy type for policy_type in policy_types: policies = list_policies_by_type(ctx, policy_type) if policies: harvested_policies.extend(policies) policies_and_rules = [] # Get all policies again including their rules if harvested_policies: for policy in harvested_policies: policy_and_rules = get_policy_object(ctx, policy["id"], rules=True) if not policy_and_rules: msg = f'Issue retrieving policy {policy["id"]} ({policy["name"]}) with rules' LOGGER.error(msg) index_event(ctx.obj.es, module=__name__, event_type="ERROR", event=msg) click.secho(f"[!] {msg}", fg="red") else: policies_and_rules.append(policy_and_rules) if policies_and_rules: if click.confirm( "[*] Do you want to print harvested policy information?", default=True): for policy in policies_and_rules: print_policy_object(policy) if click.confirm( f"[*] Do you want to save {len(policies_and_rules)} harvested policies to a file?", default=True): file_path = f"{ctx.obj.data_dir}/{ctx.obj.profile_id}_harvested_policies" write_json_file(file_path, policies_and_rules)
def delete_saved_data(ctx, profile_id): # Get a list of files associated with the configuration profile that was deleted files = list(ctx.obj.data_dir.rglob(f"{profile_id}*")) if not files: click.echo("[*] No associated saved data found for configuration profile") else: if click.confirm( f"[*] Do you want to delete the {len(files)} saved files associated with the " f"configuration profile?", default=True, ): for file in files: file.unlink() msg = f"File deleted ({file})" LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.secho(f"[*] {msg}", fg="green")
def check_assigned_roles(ctx, users): """Check if any users have admin roles assigned""" admin_users = [] msg = ( f"Checking assigned roles for {len(users)} users. This may take a while to avoid exceeding API " f"rate limits" ) LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") # Don't put print statements under click.progressbar otherwise the progress bar will be interrupted with click.progressbar(users, label="[*] Checking users for admin roles") as users: for user in users: assigned_roles, error = list_assigned_roles(ctx, user.get("id"), object_type="user", mute=True) # Stop trying to check roles if the current API token doesn't have that permission if error: return if assigned_roles: admin_user = {"user": user, "roles": assigned_roles} admin_users.append(admin_user) for role in assigned_roles: if role["type"] in ctx.obj.admin_roles: msg = f'User ID {user["id"]} has admin role {role["type"]} assigned' LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) # Sleep for 1s to avoid exceeding API rate limits time.sleep(1) if admin_users: for user in admin_users: print_role_info(user["user"]["id"], user["roles"], object_type="user") msg = f"Found {len(admin_users)} users with admin roles assigned" LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.secho(f"[*] {msg}", fg="green") file_path = f"{ctx.obj.data_dir}/{ctx.obj.profile_id}_admin_users" if click.confirm("[*] Do you want to save harvested admin user information to a file?", default=True): write_json_file(file_path, admin_users) else: msg = "No users found with admin roles assigned" LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") return admin_users
def execute(ctx): """Execute this module with the configured options""" error = check_module_options(MODULE_OPTIONS) if error: return zone_id = MODULE_OPTIONS["id"]["value"] zone = get_zone_object(ctx, zone_id) if zone: if zone["status"] == "ACTIVE": click.echo("[*] Zone is ACTIVE") if click.confirm( f'[*] Do you want to deactivate zone {zone_id} ({zone["name"]})?', default=True): msg = f'Attempting to deactivate zone {zone_id} ({zone["name"]})' LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") set_zone_state(ctx, zone["id"], operation="DEACTIVATE") elif zone["status"] == "INACTIVE": click.echo("[*] Zone is INACTIVE") if click.confirm( f'[*] Do you want to activate zone {zone_id} ({zone["name"]})?', default=True): msg = f'Attempting to activate zone {zone_id} ({zone["name"]})' LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") set_zone_state(ctx, zone["id"], operation="ACTIVATE") else: click.echo(f'[*] Policy status is {zone["status"]}')
def rename_zone(ctx, zone, original_name, new_name): """Update an existing network zone with a new name""" headers = { "Accept": "application/json", "Content-Type": "application/json", "Authorization": f"SSWS {ctx.obj.api_token}", } params = {} # Values for "type" and "name" and "gateways" OR "proxies are required when updating a network zone object payload = { "type": zone["type"], "name": new_name, "gateways": zone.get("gateways"), "proxies": zone.get("proxies") } url = f'{ctx.obj.base_url}/zones/{zone["id"]}' try: msg = f'Attempting to rename network zone "{original_name}" ({zone["id"]}) to "{new_name}"' LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") response = ctx.obj.session.put(url, headers=headers, params=params, json=payload, timeout=7) except Exception as e: LOGGER.error(e, exc_info=True) index_event(ctx.obj.es, module=__name__, event_type="ERROR", event=e) click.secho(f"[!] {URL_OR_API_TOKEN_ERROR}", fg="red") response = None if response.ok: msg = f'Network zone "{original_name}" ({zone["id"]}) changed to "{new_name}"' LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.secho(f"[*] {msg}", fg="green") get_zone_object(ctx, zone["id"]) time.sleep(1) else: msg = ( f'Error modifying network zone {zone["id"]}\n' f" Response Code: {response.status_code} | Response Reason: {response.reason}\n" f' Error Code: {response.json().get("errorCode")} | Error Summary: {response.json().get("errorSummary")}' ) LOGGER.error(msg) index_event(ctx.obj.es, module=__name__, event_type="ERROR", event=msg) click.secho(f"[!] {msg}", fg="red")
def rename_policy_rule(ctx, policy_id, rule, original_name, new_name): """Update an existing policy rule with a new name""" headers = { "Accept": "application/json", "Content-Type": "application/json", "Authorization": f"SSWS {ctx.obj.api_token}", } params = {} payload = { # Values for "type", "name", and "actions" are required when updating a policy rule "type": rule["type"], "name": new_name, "actions": rule["actions"], } url = f'{ctx.obj.base_url}/policies/{policy_id}/rules/{rule["id"]}' try: msg = f'Attempting to rename rule "{original_name}" ({rule["id"]}) to "{new_name}" in policy {policy_id}' LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") response = ctx.obj.session.put(url, headers=headers, params=params, json=payload, timeout=7) except Exception as e: LOGGER.error(e, exc_info=True) index_event(ctx.obj.es, module=__name__, event_type="ERROR", event=e) click.secho(f"[!] {URL_OR_API_TOKEN_ERROR}", fg="red") response = None if response.ok: msg = f'Rule "{original_name}" ({rule["id"]}) changed to "{new_name}" in policy {policy_id}' LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.secho(f"[*] {msg}", fg="green") get_policy_object(ctx, policy_id) time.sleep(1) else: msg = ( f'Error modifying rule "{original_name}" {rule["id"]} in policy {policy_id}\n' f" Response Code: {response.status_code} | Response Reason: {response.reason}\n" f' Error Code: {response.json().get("errorCode")} | Error Summary: {response.json().get("errorSummary")}' ) LOGGER.error(msg) index_event(ctx.obj.es, module=__name__, event_type="ERROR", event=msg) click.secho(f"[!] {msg}", fg="red")
def execute(ctx): """Execute this module with the configured options""" error = check_module_options(MODULE_OPTIONS) if error: return user_id = MODULE_OPTIONS["id"]["value"] click.echo("""[*] Attempting to retrieve user's current state""") error = get_user_object(ctx, user_id) if error: return click.echo("[*] Available lifecycle operations:") for index, operation in enumerate(LIFECYCLE_OPERATIONS): click.echo( f'{index + 1}. {operation["operation"]} - {operation["description"]}' ) while True: choice = click.prompt( "[*] Which state do you want to transition the user to?", type=int) if (choice > 0) and (choice <= len(LIFECYCLE_OPERATIONS)): lifecycle_operation = LIFECYCLE_OPERATIONS[choice - 1]["operation"] msg = f"Attempting to {lifecycle_operation} user ID {user_id}" LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") execute_lifecycle_operation(ctx, user_id, lifecycle_operation) return else: click.secho("[!] Invalid option selected", fg="red")
def execute(ctx): """Execute this module with the configured options""" error = check_module_options(MODULE_OPTIONS) if error: return msg = f'Attempting to get policy object for policy ID {MODULE_OPTIONS["id"]["value"]}' LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") policy = get_policy_object(ctx, MODULE_OPTIONS["id"]["value"], rules=True) if policy: print_policy_object(policy) if click.confirm( f"[*] Do you want to save policy {policy['id']} ({policy['name']}) to a file?", default=True): file_path = f'{ctx.obj.data_dir}/{ctx.obj.profile_id}_policy_{policy["id"]}' write_json_file(file_path, policy)
def execute(ctx): """Execute this module with the configured options""" error = check_module_options(MODULE_OPTIONS) if error: return admin_roles = ctx.obj.admin_roles user_id = MODULE_OPTIONS["id"]["value"] click.echo("[*] Available admin roles:") for index, role in enumerate(admin_roles): click.echo(f"{index + 1}. {role}") while True: choice = click.prompt( "[*] Which admin role do you want to assign to the user?", type=int) if (choice > 0) and (choice <= len(admin_roles)): role_type = admin_roles[choice - 1] msg = f"Attempting to assign admin role, {role_type} to user ID, {user_id}" LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") assign_admin_role(ctx, user_id, role_type, target="user") return else: click.secho("[!] Invalid option selected", fg="red")
def execute(ctx): """Execute this module with the configured options""" error = check_module_options(MODULE_OPTIONS) if error: return password = click.prompt( "[*] Enter a password for the new user. The input for this value is hidden", hide_input=True) msg = f'Attempting to create new Okta user {MODULE_OPTIONS["login"]["value"]}' LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") url = f"{ctx.obj.base_url}/users" headers = { "Accept": "application/json", "Content-Type": "application/json", "Authorization": f"SSWS {ctx.obj.api_token}", } # Activate the new user when it's created params = {"activate": "true"} payload = { "profile": { "firstName": MODULE_OPTIONS["first_name"]["value"], "lastName": MODULE_OPTIONS["last_name"]["value"], "email": MODULE_OPTIONS["email"]["value"], "login": MODULE_OPTIONS["login"]["value"], }, "groupIds": MODULE_OPTIONS["group_ids"]["value"], "credentials": { "password": { "value": password } }, } try: response = ctx.obj.session.post(url, headers=headers, params=params, json=payload, timeout=7) except Exception as e: LOGGER.error(e, exc_info=True) index_event(ctx.obj.es, module=__name__, event_type="ERROR", event=e) click.secho(f"[!] {URL_OR_API_TOKEN_ERROR}", fg="red") response = None if response.ok: msg = f'Created new Okta user {MODULE_OPTIONS["login"]["value"]}' LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.secho(f"[*] {msg}", fg="green") else: msg = ( f"Error creating new Okta user\n" f" Response Code: {response.status_code} | Response Reason: {response.reason}\n" f' Error Code: {response.json().get("errorCode")} | Error Summary: {response.json().get("errorSummary")}' ) LOGGER.error(msg) index_event(ctx.obj.es, module=__name__, event_type="ERROR", event=msg) click.secho(f"[!] {msg}", fg="red") click.echo( 'Did you try and add the new user to a built-in group? E.g. "Everyone"' ) return
def execute(ctx): """Execute this module with the configured options""" error = check_module_options(MODULE_OPTIONS) if error: return user_id = MODULE_OPTIONS["id"]["value"] enrolled_factors, error = list_enrolled_factors(ctx, user_id) if error: return if not enrolled_factors: msg = f"No enrolled MFA factors found for user {user_id}" LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") return else: msg = f"Found {len(enrolled_factors)} enrolled MFA factors for user {user_id}" LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.secho(f"[*] {msg}", fg="green") # Print the user's enrolled factors factors = [] for index, factor in enumerate(enrolled_factors): factors.append(( index + 1, factor["id"], factor.get("factorType", "-"), factor.get("provider", "-"), factor.get("vendorName", "-"), factor.get("status", "-"), )) headers = [ "#", "Factor ID", "Type", "Provider", "Vendor Name", "Status" ] click.echo(tabulate(factors, headers=headers, tablefmt="pretty")) # Prompt to delete a factor while True: if click.confirm( "[*] Do you want to delete a MFA factor from the user's profile?", default=True): choice = click.prompt( "[*] Enter the number (#) of the MFA factor to delete", type=int) if (choice > 0) and (choice <= len(factors)): factor_id = enrolled_factors[choice - 1]["id"] reset_factor(ctx, user_id, factor_id) return else: click.secho("[!] Invalid choice", fg="red") return else: return
def execute(ctx): """Execute this module with the configured options""" """ Can't I just make an API call to get all users that have a specific role assigned? Good question. There is no Okta API yet to get all users that have a specific role assigned to them. It is necessary to enumerate through all Okta users and then get the roles assigned to each user """ options = ( "[*] Available options\n" "[1] Load harvested users from json file and check their assigned roles for administrator permissions\n" "[2] Harvest all users and check their assigned roles for administrator permissions\n" "[0] Exit this menu\n" "[*] Choose from the above options" ) while True: value = click.prompt(options, type=int) if value == 1: file_path = Path( click.prompt( "[*] Enter full path of file containing harvested Okta users", ) ) if file_path.exists(): msg = f"Attempting to check roles for users in file, {file_path}" LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") users = load_json_file(file_path) check_assigned_roles(ctx, users) return else: msg = f"File not found, {file_path}" LOGGER.error(msg) index_event(ctx.obj.es, module=__name__, event_type="ERROR", event=msg) click.secho(f"[!] {msg}", fg="red") elif value == 2: if click.confirm( "[*] Do you want to attempt to harvest information for all users? This may take a while " "to avoid exceeding API rate limits", default=True, ): msg = "Attempting to harvest all Okta users" LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") users = list_users(ctx) check_assigned_roles(ctx, users) return elif value == 0: return else: click.secho("[!] Invalid option selected", fg="red")
def execute(ctx): """Execute this module with the configured options""" options = ( "[*] Available options\n" "[1] Load harvested groups from json file and check their assigned roles for administrator " "permissions\n" "[2] Harvest all groups and check their assigned roles for administrator permissions\n" "[0] Exit this menu\n" "[*] Choose from the above options") while True: value = click.prompt(options, type=int) if value == 1: file_path = Path( click.prompt( "[*] Enter full path of file containing harvested Okta groups", )) if file_path.exists(): msg = f"Attempting to check roles for groups in file, {file_path}" LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") groups = load_json_file(file_path) check_assigned_roles(ctx, groups) return else: msg = f"File not found, {file_path}" LOGGER.error(msg) index_event(ctx.obj.es, module=__name__, event_type="ERROR", event=msg) click.secho(f"[!] {msg}", fg="red") elif value == 2: if click.confirm( "[*] Do you want to attempt to harvest information for all groups?", default=True): msg = "Attempting to harvest all Okta groups" LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") groups = list_groups(ctx) check_assigned_roles(ctx, groups) return elif value == 0: return else: click.secho("[!] Invalid option selected", fg="red")
def execute(ctx): """Execute this module with the configured options""" error = check_module_options(MODULE_OPTIONS) if error: return msg = f'Attempting to generate a one-time token to reset the password for user ID {MODULE_OPTIONS["id"]["value"]}' LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") url = f'{ctx.obj.base_url}/users/{MODULE_OPTIONS["id"]["value"]}/lifecycle/reset_password' headers = { "Accept": "application/json", "Content-Type": "application/json", "Authorization": f"SSWS {ctx.obj.api_token}", } # Set sendEmail to False. The default value for sendEmail is True, which will send the one-time token to the # target user params = {"sendEmail": "False"} payload = {} try: response = ctx.obj.session.post(url, headers=headers, params=params, json=payload, timeout=7) except Exception as e: LOGGER.error(e, exc_info=True) index_event(ctx.obj.es, module=__name__, event_type="ERROR", event=e) click.secho(f"[!] {URL_OR_API_TOKEN_ERROR}", fg="red") response = None if response.ok: msg = f'One-time password reset token generated for user {MODULE_OPTIONS["id"]["value"]}' LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.secho(f"[*] {msg}", fg="green") click.echo( "[*] The user will have the status of RECOVERY and will not be able to login or initiate the " "forgot password flow until the password is reset") response = response.json() click.echo(f'Reset password URL: {response["resetPasswordUrl"]}') else: msg = ( f"Error resetting password for user\n" f" Response Code: {response.status_code} | Response Reason: {response.reason}\n" f' Error Code: {response.json().get("errorCode")} | Error Summary: {response.json().get("errorSummary")}' ) LOGGER.error(msg) index_event(ctx.obj.es, module=__name__, event_type="ERROR", event=msg) click.secho(f"[!] {msg}", fg="red") click.echo( "Check the status of the user. The user's status must be ACTIVE") return
def execute(ctx): """Execute this module with the configured options""" options = ( "[*] Available options\n" "[1] Load harvested users from a json file and check their enrolled MFA factors\n" "[2] Harvest all users and check their enrolled MFA factors\n" "[0] Exit this menu\n" "[*] Choose from the above options") while True: value = click.prompt(options, type=int) if value == 1: file_path = Path( click.prompt( "[*] Enter full path of file containing harvested Okta users", )) if file_path.exists(): msg = f"Attempting to check MFA factors for users in file, {file_path}" LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") users = load_json_file(file_path) check_enrolled_factors(ctx, users) return else: msg = f"File not found, {file_path}" LOGGER.error(msg) index_event(ctx.obj.es, module=__name__, event_type="ERROR", event=msg) click.secho(f"[!] {msg}", fg="red") elif value == 2: if click.confirm( "[*] Do you want to attempt to harvest information for all users? This may take a while " "to avoid exceeding API rate limits", default=True, ): msg = "Attempting to harvest all Okta users" LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") users = list_users(ctx) check_enrolled_factors(ctx, users) return elif value == 0: return else: click.secho("[!] Invalid option selected", fg="red")
def check_enrolled_factors(ctx, users): """Check for users that have no MFA factors enrolled""" users_without_mfa = [] msg = ( f"Checking enrolled MFA factors for {len(users)} users. This may take a while to avoid exceeding API " f"rate limits") LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") # Don't put print statements under click.progressbar otherwise the progress bar will be interrupted with click.progressbar( users, label="[*] Checking for users without MFA enrolled") as users: for user in users: factors, error = list_enrolled_factors(ctx, user.get("id"), mute=True) # Stop trying to check enrolled MFA factors if the current API token doesn't have that permission if error: return if not factors: users_without_mfa.append(user) msg = f'User {user["id"]} does not have any MFA factors enrolled' LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) # Sleep for 1s to avoid exceeding API rate limits time.sleep(1) if users_without_mfa: msg = f"Found {len(users_without_mfa)} users without any MFA factors enrolled" LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.secho(f"[*] {msg}", fg="green") if click.confirm( "[*] Do you want to print information for users without MFA?", default=True): for user in users_without_mfa: print_user_info(user) if click.confirm( "[*] Do you want to save users without any MFA factors enrolled to a file?", default=True): file_path = f"{ctx.obj.data_dir}/{ctx.obj.profile_id}_users_without_mfa" write_json_file(file_path, users_without_mfa) else: msg = "No users found without any MFA factors enrolled" LOGGER.info(msg) index_event(ctx.obj.es, module=__name__, event_type="INFO", event=msg) click.echo(f"[*] {msg}") return users_without_mfa