DUO Admin
DUO for admins. Must have access to the admin api in order to use this.
Authentication & Identity Management · DUO Admin
Details
| ID | DUO Admin |
|---|---|
| Provider | Cisco Systems |
| Category | Authentication & Identity Management |
| From Version | 5.0.0 |
| Docker Image | demisto/vendors-sdk:1.0.0.10120494 |
| Supported Modules | Agentix XSIAM |
README
DUO for admins.
Must have access to the admin api in order to use this.
This integration was integrated and tested with version 4.4.0 of DUO Admin
Configure DUO Admin in Cortex
| Parameter | Required |
|---|---|
| API Hostname | True |
| Integration Key | True |
| Secret Key | True |
| Trust any certificate (not secure) | False |
| Use system proxy settings | False |
Commands
You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.
duoadmin-get-authentication-logs-by-user
Returns authentication logs associated with a user. Limited to 30 at a time
Base Command
duoadmin-get-authentication-logs-by-user
Input
| Argument Name | Description | Required |
|---|---|---|
| username | The user associated with the logs. | Required |
| from | Fetch logs from this time until now. Possible values are: 10_seconds_ago, 1_minute_ago, 10_minutes_ago, 1_hour_ago, 10_hours_ago, 1_day_ago, 1_week_ago, 1_month_ago, 1_year_ago, 5_years_ago, 10_years_ago. | Required |
| limit | The maximum number of authentication logs to return. Default is 50. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| DuoAdmin.UserDetails.auth_logs.result | string | Result of the authentication attempt |
| DuoAdmin.UserDetails.auth_logs.event_type | string | Type of activity logged |
| DuoAdmin.UserDetails.auth_logs.reason | string | Reason for the authentication attempt result |
| DuoAdmin.UserDetails.auth_logs.access_device.ip | string | The GeoIP location of the access device. IP field |
| DuoAdmin.UserDetails.auth_logs.access_device.hostname | string | The GeoIP location of the access device. Hostname field |
| DuoAdmin.UserDetails.auth_logs.access_device.location.city | string | The GeoIP location of the access device. City field |
| DuoAdmin.UserDetails.auth_logs.access_device.location.state | string | The GeoIP location of the access device. State field |
| DuoAdmin.UserDetails.auth_logs.access_device.location.country | string | The GeoIP location of the access device. Country field |
| DuoAdmin.UserDetails.auth_logs.auth_device.ip | string | The GeoIP location of the authentication device. IP field |
| DuoAdmin.UserDetails.auth_logs.auth_device.hostname | string | The GeoIP location of the authentication device. Hostname field |
| DuoAdmin.UserDetails.auth_logs.auth_device.location.city | string | The GeoIP location of the authentication device. City field |
| DuoAdmin.UserDetails.auth_logs.auth_device.location.state | string | The GeoIP location of the authentication device. State field |
| DuoAdmin.UserDetails.auth_logs.auth_device.location.country | string | The GeoIP location of the authentication device. Country field |
| DuoAdmin.UserDetails.auth_logs.timestamp | date | Timestamp of the event |
| DuoAdmin.UserDetails.auth_logs.application.name | string | Name of the application accessed |
| DuoAdmin.UserDetails.auth_logs.factor | string | The authentication factor |
duoadmin-dissociate-device-from-user
Dissociates a device from a user
Base Command
duoadmin-dissociate-device-from-user
Input
| Argument Name | Description | Required |
|---|---|---|
| username | user to dissociate a device from. | Required |
| device_id | the device id to dissociate. | Required |
Context Output
There is no context output for this command.
duoadmin-delete-u2f-token
Delete a u2f token
Base Command
duoadmin-delete-u2f-token
Input
| Argument Name | Description | Required |
|---|---|---|
| token_id | the id of the token to delete. | Required |
Context Output
There is no context output for this command.
duoadmin-get-users
Return usernames and their user id
Base Command
duoadmin-get-users
Input
| Argument Name | Description | Required |
| — | — | — |
Context Output
| Path | Type | Description |
|---|---|---|
| DuoAdmin.UserDetails.username | string | Username |
| DuoAdmin.UserDetails.user_id | string | User Id |
| DuoAdmin.UserDetails.status | string | Status |
| DuoAdmin.UserDetails.is_enrolled | boolean | is_enrolled |
| DuoAdmin.UserDetails.last_login | date | Last_login |
| DuoAdmin.UserDetails.realname | string | Real Name |
| DuoAdmin.UserDetails.email | string | |
| DuoAdmin.UserDetails.phones | unknown | Phone numbers |
duoadmin-get-devices-by-user
Return devices associated with a user
Base Command
duoadmin-get-devices-by-user
Input
| Argument Name | Description | Required |
|---|---|---|
| username | Username. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| DuoAdmin.UserDetails.phones.phone_id | string | Device Id |
| DuoAdmin.UserDetails.phones.number | string | Device number |
| DuoAdmin.UserDetails.phones.platform | string | Device platform |
| DuoAdmin.UserDetails.phones.last_seen | date | Last time the device was used |
duoadmin-get-u2f-tokens-by-user
Returns a list of U2F tokens associated with the given username
Base Command
duoadmin-get-u2f-tokens-by-user
Input
| Argument Name | Description | Required |
|---|---|---|
| username | username. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| DuoAdmin.UserDetails.u2ftokens | Unknown | The list of tokens |
duoadmin-get-devices
Returns all existing devices
Base Command
duoadmin-get-devices
Input
| Argument Name | Description | Required |
| — | — | — |
Context Output
| Path | Type | Description |
|---|---|---|
| DuoAdmin.Phones.phone_id | Unknown | Device Id |
| DuoAdmin.Phones.number | Unknown | Device number |
| DuoAdmin.Phones.platform | Unknown | Device platform |
| DuoAdmin.Phones.last_seen | Unknown | Last time the device was used |
| DuoAdmin.Phones.users | Unknown | Users associated with this device |
duoadmin-associate-device-to-user
Associates a device to a user
Base Command
duoadmin-associate-device-to-user
Input
| Argument Name | Description | Required |
|---|---|---|
| username | Username. | Required |
| device_id | Device Id. | Required |
Context Output
There is no context output for this command.
duoadmin-get-admins
Returns administrator accounts
Base Command
duoadmin-get-admins
Input
| Argument Name | Description | Required |
| — | — | — |
Context Output
| Path | Type | Description |
|---|---|---|
| DuoAdmin.UserDetails.admin_id | string | Admin_id |
| DuoAdmin.UserDetails.admin_units | unknown | Admin Units |
| DuoAdmin.UserDetails.created | date | Created |
| DuoAdmin.UserDetails.email | string | |
| DuoAdmin.UserDetails.last_login | date | Last Login |
| DuoAdmin.UserDetails.name | string | Name |
| DuoAdmin.UserDetails.phone | unknown | Phone |
| DuoAdmin.UserDetails.role | string | Admin Role |
| DuoAdmin.UserDetails.status | string | Admin Status |
duoadmin-get-bypass-codes
Retrieves the information from the bypass code table
Base Command
duoadmin-get-bypass-codes
Input
| Argument Name | Description | Required |
| — | — | — |
Context Output
| Path | Type | Description |
|---|---|---|
| DuoAdmin.UserDetails.bypass_code_id | unknown | Bypass Code Id |
| DuoAdmin.UserDetails.admin_email | string | Admin Email |
| DuoAdmin.UserDetails.created | date | Bypass Created |
| DuoAdmin.UserDetails.expiration | unknown | Bypass Expiration |
| DuoAdmin.UserDetails.reuse_count | unknown | Bypass Reuse Count |
| DuoAdmin.UserDetails.user.username | unknown | Username |
| DuoAdmin.UserDetails.user.created | unknown | Created |
| DuoAdmin.UserDetails.user.email | unknown | |
| DuoAdmin.UserDetails.user.last_login | unknown | Last Login |
| DuoAdmin.UserDetails.user.status | unknown | Status |
| DuoAdmin.UserDetails.user.user_id | unknown | User Id |
duoadmin-modify-admin
Modify the administrator user.
Base Command
duoadmin-modify-admin
Input
| Argument Name | Description | Required |
|---|---|---|
| admin_id | The id of the admin. | Required |
| name | The name of the admin. | Optional |
| phone | The phone number of the admin. | Optional |
| password | the password of the admin. | Optional |
| password_change_required | a flag to determine if the password should change. Possible values are: false, true. Default is false. | Optional |
Context Output
There is no context output for this command.
duoadmin-modify-user
Modify the user account.
Base Command
duoadmin-modify-user
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | The user id of the user. | Required |
| user_name | The user name of the user. | Optional |
| realname | The real name of the user. | Optional |
| status | The status of the user. Possible values are: active, disabled. | Optional |
| notes | Notes for the user. | Optional |
| The email of the user. | Optional | |
| first_name | The first name of the user. | Optional |
| last_name | The last name of the user. | Optional |
| alias1 | The first alias of the user. | Optional |
| alias2 | The second alias of the user. | Optional |
| alias3 | The third alias of the user. | Optional |
| alias4 | The fourth alias of the user. | Optional |
| aliases | The aliases list of the user. | Optional |
Context Output
There is no context output for this command.
Configuration parameters
hostname— API Hostname (required)integration_key— Integration Keysecret_key— Secret Keycredentials_key— Integration Keyinsecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (12)
-
duoadmin-associate-device-to-userAssociates a device to a user.
-
duoadmin-delete-u2f-tokenDelete a u2f token.
-
duoadmin-dissociate-device-from-userDissociates a device from a user.
-
duoadmin-get-adminsReturns administrator accounts.
-
duoadmin-get-authentication-logs-by-userReturns authentication logs associated with a user. Limited to 30 at a time.
-
duoadmin-get-bypass-codesRetrieves the information from the bypass code table.
-
duoadmin-get-devicesReturns all existing devices.
-
duoadmin-get-devices-by-userReturn devices associated with a user.
-
duoadmin-get-u2f-tokens-by-userReturns a list of U2F tokens associated with the given username.
-
duoadmin-get-usersReturn usernames and their user id.
-
duoadmin-modify-adminModify the administrator user.
-
duoadmin-modify-userModify the user account.
# imports import calendar import demistomock as demisto # noqa: F401 import duo_client from CommonServerPython import * # noqa: F401 # Setup HOST = demisto.getParam("hostname") INTEGRATION_KEY = demisto.params().get("credentials_key", {}).get("identifier") or demisto.getParam("integration_key") SECRET_KEY = demisto.params().get("credentials_key", {}).get("password") or demisto.getParam("secret_key") USE_SSL = not demisto.params().get("insecure", False) USE_PROXY = demisto.params().get("proxy", False) # The duo client returns a signature error upon bad secret # Convert it to a more informative message using this INVALID_SECRET_ERROR_STRING = "Invalid signature in request credentials" # Maps OPTIONS_TO_TIME = { "10_seconds_ago": datetime.now() - timedelta(seconds=10), # left here for backwards compatability "1_minutes_ago": datetime.now() - timedelta(minutes=1), "1_minute_ago": datetime.now() - timedelta(minutes=1), "10_minutes_ago": datetime.now() - timedelta(minutes=10), "1_hour_ago": datetime.now() - timedelta(hours=1), "10_hours_ago": datetime.now() - timedelta(hours=10), "1_day_ago": datetime.now() - timedelta(days=1), "1_week_ago": datetime.now() - timedelta(days=7), "1_month_ago": datetime.now() - timedelta(days=30), "1_year_ago": datetime.now() - timedelta(days=365), "5_years_ago": datetime.now() - timedelta(days=1825), "10_years_ago": datetime.now() - timedelta(days=3650), } def override_make_request(self, method, uri, body, headers): # pragma: no cover """ This function is an override function to the original duo_client.client.Client._make_request function in API version 4.1.0 The reason for it is that the API creates a bad uri address for the GET requests. """ conn = self._connect() conn.request(method, uri, body, headers) response = conn.getresponse() data = response.read() self._disconnect(conn) return (response, data) # Utility Methods def create_api_call(): if USE_SSL: client = duo_client.Admin( ikey=INTEGRATION_KEY, skey=SECRET_KEY, host=HOST, ) else: client = duo_client.Admin(ikey=INTEGRATION_KEY, skey=SECRET_KEY, host=HOST, ca_certs="DISABLE") try: client._make_request = lambda method, uri, body, headers: override_make_request(client, method, uri, body, headers) except Exception as e: demisto.error(f"Error making request - failed to create client: {e}") raise Exception return client def set_proxy(admin_api): # pragma: no cover try: proxy_settings = os.environ.get("HTTP_PROXY") or os.environ.get("http_proxy", "") if proxy_settings: host, port = get_host_port_from_proxy_settings(proxy_settings) if USE_PROXY: admin_api.set_proxy(host=host.strip(":"), port=port) # if no proxy settings have been set except ValueError: admin_api.set_proxy(host=None, port=None, proxy_type=None) except Exception as e: demisto.error(f"Error setting proxy: {e}") raise Exception def get_host_port_from_proxy_settings(proxy_settings): # pragma: no cover proxy_settings_str = str(proxy_settings) port = proxy_settings_str.split(":")[-1] host_regex_filter = re.search(ipv4Regex, proxy_settings_str) if host_regex_filter: host = host_regex_filter.group() else: proxy_settings_str_args = proxy_settings_str.split(":") if "http" in proxy_settings_str: host = ":".join(proxy_settings_str_args[1:-1])[2:] else: host = ":".join(proxy_settings_str_args[0:-1]) return host, port def time_to_timestamp_milliseconds(time): # pragma: no cover return str(calendar.timegm(time.utctimetuple()) * 1000) # Generic function that receives a result json, and turns it into an entryObject def get_entry_for_object(title, obj, contents, context, headers=None): if len(obj) == 0: return { "Type": entryTypes["note"], "Contents": contents, "ContentsFormat": formats["json"], "HumanReadable": "There is no output result", "EntryContext": context, } if headers: if isinstance(headers, str): headers = headers.split(",") if isinstance(obj, dict): headers = list(set(headers).intersection(set(obj.keys()))) readable = tableToMarkdown(title, obj, headers, lambda h: h.title().replace("_", " ").replace(".", ":")) return { "Type": entryTypes["note"], "Contents": obj, "ContentsFormat": formats["json"], "ReadableContentsFormat": formats["markdown"], "HumanReadable": readable, "EntryContext": context, } def get_user_id(admin_api, username): res = admin_api.get_users_by_name(username) if len(res) == 0: return_error("No users found with the given username") return res[0]["user_id"] # Methods # Duo client return 2 different known structures of error messages def test_instance(admin_api): try: admin_api.get_users(limit=1) demisto.results("ok") except Exception as e: if hasattr(e, "data"): # error data for 40103 is not informative enough so we write our own if e.__getattribute__("data")["code"] == 40103: raise Exception("Invalid secret key in request credentials") else: raise Exception(e.__getattribute__("data")["message"]) elif hasattr(e, "strerror"): raise Exception(e.__getattribute__("strerror")) else: raise Exception("Unknown error: " + str(e)) def get_all_users(admin_api): res = admin_api.get_users() entry = get_entry_for_object( "Users", res, res, {"DuoAdmin.UserDetails(val.username==obj.username)": res}, headers=["username", "user_id", "is_enrolled", "last_login", "realname", "email", "phones", "status"], ) demisto.results(entry) def get_authentication_logs_by_user(admin_api, args): user_name = args.get("username") min_time = args.get("from") limit = demisto.args().get("limit", "50") res = admin_api.get_authentication_log( 2, users=get_user_id(admin_api, user_name), mintime=time_to_timestamp_milliseconds(OPTIONS_TO_TIME[min_time]), maxtime=time_to_timestamp_milliseconds(datetime.now()), limit=limit, ) raw_logs = res["authlogs"] for log in raw_logs: log["timestamp"] = formatEpochDate(log["timestamp"]) entry = get_entry_for_object( "Authentication logs for " + user_name, raw_logs, raw_logs, {"DuoAdmin.UserDetails(val.username && val.username == obj.username)": {"username": user_name, "auth_logs": raw_logs}}, headers=["access_device", "event_type", "result", "reason", "application", "factor", "timestamp", "auth_device"], ) demisto.results(entry) def get_devices_by_user(admin_api, args): user_name = args.get("username") user_id = get_user_id(admin_api, user_name) res = admin_api.get_user_phones(user_id) entry = get_entry_for_object( f"Devices for {user_name}", res, res, {"DuoAdmin.UserDetails(val.username && val.username == obj.username)": {"username": user_name, "phones": res}}, ) demisto.results(entry) def get_all_devices(admin_api): res = admin_api.get_phones() entry = get_entry_for_object("Devices", res, res, {"DuoAdmin.Phones(val.phone_id==obj.phone_id)": res}) demisto.results(entry) def dissociate_device_by_user(admin_api, args): user_name = args.get("username") device_id = args.get("device_id") user_id = get_user_id(admin_api, user_name) admin_api.delete_user_phone(user_id, device_id) demisto.results(f"Phone with ID {device_id} was dissociated to user {user_name}") def associate_device_to_user(admin_api, args): user_name = args.get("username") device_id = args.get("device_id") user_id = get_user_id(admin_api, user_name) admin_api.add_user_phone(user_id, device_id) demisto.results(f"Phone with ID {device_id} was associated to user {user_name}") def get_u2f_tokens_by_user(admin_api, args): user_name = args.get("username") user_id = get_user_id(admin_api, user_name) res = admin_api.get_user_u2ftokens(user_id) for token in res: token["date_added"] = formatEpochDate(token["date_added"]) entry = get_entry_for_object( "U2F Tokens for " + user_name, res, res, {"DuoAdmin.UserDetails(val.username && val.username == obj.username)": {"username": user_name, "u2ftokens": res}}, ) demisto.results(entry) def delete_u2f_token(admin_api, args): token_id = args.get("token_id") admin_api.delete_u2ftoken(token_id) demisto.results(f"Token with ID {token_id} deleted successfully") def get_all_bypass_codes(admin_api): res = admin_api.get_bypass_codes() entry = get_entry_for_object( "Bypass", res, res, {"DuoAdmin.UserDetails(val.bypass_code_id==obj.bypasscodeid)": res}, headers=[ "bypass_code_id", "admin_email", "expiration", "reuse_count", "user.created", "user.email", "user.last_login", "user.status", "user.user_id", "user.username", ], ) demisto.results(entry) def get_all_admins(admin_api): res = admin_api.get_admins() entry = get_entry_for_object( "Admins", res, res, {"DuoAdmin.AdminDetails(val.name==obj.name)": res}, headers=["admin_id", "admin_units", "created", "email", "last_login", "name", "phone", "role", "status"], ) demisto.results(entry) def modify_admin_user(admin_api, admin_id=None, name=None, phone=None, password=None, password_change_required=None): admin_api.update_admin(admin_id, name, phone, password, password_change_required) return CommandResults(readable_output=f"The admin id {admin_id} successfully updated") def modify_user( admin_api, user_id=None, user_name=None, real_name=None, status=None, notes=None, email=None, first_name=None, last_name=None, alias1=None, alias2=None, alias3=None, alias4=None, aliases=None, ): admin_api.update_user( user_id, user_name, real_name, status, notes, email, first_name, last_name, alias1, alias2, alias3, alias4, argToList(aliases), ) return CommandResults(readable_output=f"The user id {user_id} successfully updated") def main() -> None: # pragma: no cover args = demisto.args() command = demisto.command() demisto.debug(f"Command being called is {command}") if not (SECRET_KEY and INTEGRATION_KEY): raise DemistoException("Secret Key and Integration Key must be provided.") try: admin_api = create_api_call() set_proxy(admin_api) if command == "test-module": test_instance(admin_api) elif command == "duoadmin-get-users": get_all_users(admin_api) elif command == "duoadmin-get-admins": get_all_admins(admin_api) elif command == "duoadmin-get-bypass-codes": get_all_bypass_codes(admin_api) elif command == "duoadmin-get-authentication-logs-by-user": get_authentication_logs_by_user(admin_api, args) elif command == "duoadmin-get-devices": get_all_devices(admin_api) elif command == "duoadmin-get-devices-by-user": get_devices_by_user(admin_api, args) elif command == "duoadmin-associate-device-to-user": associate_device_to_user(admin_api, args) elif command == "duoadmin-dissociate-device-from-user": dissociate_device_by_user(admin_api, args) elif command == "duoadmin-get-u2f-tokens-by-user": get_u2f_tokens_by_user(admin_api, args) elif command == "duoadmin-delete-u2f-token": delete_u2f_token(admin_api, args) elif command == "duoadmin-modify-user": return_results(modify_user(admin_api, **demisto.args())) elif command == "duoadmin-modify-admin": return_results(modify_admin_user(admin_api, **demisto.args())) else: raise NotImplementedError(f"{command} command is not implemented.") except Exception as e: return_error(f"Failed to execute {command} command.\nError:\n{e!s}") if __name__ in ("__main__", "__builtin__", "builtins"): main()