Okta IAM

Integrate with Okta's Identity Access Management service to execute CRUD operations to employee lifecycle processes.

Authentication & Identity Management · Okta

Details

IDOkta IAM
ProviderOkta
CategoryAuthentication & Identity Management
From Version6.0.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix Cloud Runtime Security Cloud Posture Security XSIAM EDR Cortex Cloud

README

Note: This integration should be used along with our ILM premium pack. For further details, visit our ILM pack documentation.

Integrate with Okta’s Identity Access Management service to execute CRUD operations to employee lifecycle processes.
This integration was integrated and tested with version v1 of the Okta integration.
For more information, refer to the Identity Lifecycle Management article.

Configure Okta IAM in Cortex

Parameter Description Required
url Okta URL (https://<domain>.okta.com) True
apitoken API Token (see Detailed Instructions) True
insecure Trust any certificate (not secure) False
proxy Use system proxy settings False
create-user-enabled Allow creating users False
update-user-enabled Allow updating users False
enable-user-enabled Allow enabling users False
disable-user-enabled Allow disabling users False
create-if-not-exists Automatically create user if not found in update command False
mapper-in Incoming Mapper True
mapper-out Outgoing Mapper True
max_fetch Fetch Limit (recommended less than 200) False
isFetch Fetch incidents False
incidentFetchInterval Incidents Fetch Interval False
incidentType Incident type False
auto_generate_query_filter Query only application events configured in the IAM Configuration False
fetch_query_filter Fetch Query Filter (Okta system log events) True
first_fetch First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days) False
  • To allow the integration to access the mapper from within the code, as required by the ILM pack, both mappers have to be configured in their proper respective fields and not in the “Mapper (outgoing)” dropdown list selector.

Fetch incidents using an “IAM - Configuration” incident

When the “Query only application events configured in IAM Configuration” checkbox is selected, add or remove event types for the applications you configured in the IAM Configuration incident are retrieved. You must have at least one application configured in XSOAR to fetch incidents from Okta.

Fetch incidents using a manual query filter expression

Note: Cortex XSOAR recommends you use the Query only application events configured in IAM Configuration option to generate the fetch-incidents query filter. The following following method should be used primarily for debugging purposes.
Clear the “Query only application events configured in IAM Configuration” checkbox to use a custom fetch query filter expression. The expression must be in SCIM syntax, and include the add and remove event types, as well as the application ID.
For example: (eventType eq "application.user_membership.add" or eventType eq "application.user_membership.remove") and target.id eq "0oar418fvkm67MWGd0h7"
You may also use the advanced search in Okta’s System Logs to generate the filter expression.
For more details, visit Okta API reference.

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.

iam-create-user


Creates a user.

Base Command

iam-create-user

Input

Argument Name Description Required
user-profile User Profile indicator details. Required
allow-enable When set to true, after the command execution the status of the user in the 3rd-party integration will be active. Optional

Context Output

Path Type Description
IAM.Vendor.active Boolean When true, indicates that the employee’s status is active in the 3rd-party integration.
IAM.Vendor.brand String Name of the integration.
IAM.Vendor.details string Provides the raw data from the 3rd-party integration.
IAM.Vendor.email String The employee’s email address.
IAM.Vendor.errorCode Number HTTP error response code.
IAM.Vendor.errorMessage String Reason why the API failed.
IAM.Vendor.id String The employee’s user ID in the app.
IAM.Vendor.instanceName string Name of the integration instance.
IAM.Vendor.success Boolean When true, indicates that the command was executed successfully.
IAM.Vendor.username String The employee’s username in the app.

Command Example

!iam-create-user user-profile={\"email\":\"testdemisto2@paloaltonetworks.com\", \"surname\":\"Test\",\"givenname\":\"Demisto\"}

Human Readable Output

Create User Results (Okta IAM)
brand instanceName success active id username email details
Okta IAM Okta IAM_instance_1 true true 00uujxnbh3uJw4tWA0h7 testdemisto2@paloaltonetworks.com testdemisto2@paloaltonetworks.com id: 00uujxnbh3uJw4tWA0h7
status: PROVISIONED
created: 2020-10-18T17:54:30.000Z
activated: 2020-10-18T17:54:30.000Z
statusChanged: 2020-10-18T17:54:30.000Z
lastLogin: null
lastUpdated: 2020-10-18T17:54:30.000Z
passwordChanged: null
type: {“id”: “oty8zfz6plq7b0r830h7”}
profile: {“firstName”: “Demisto”, “lastName”: “Test”, “mobilePhone”: null, “secondEmail”: null, “login”: “testdemisto2@paloaltonetworks.com”, “email”: “testdemisto44@paloaltonetworks.com”}
credentials: {“provider”: {“type”: “OKTA”, “name”: “OKTA”}}
_links: {“suspend”: {“href”: “https://panw-test.oktapreview.com/api/v1/users/00uujxnbh3uJw4tWA0h7/lifecycle/suspend”, “method”: “POST”}, “schema”: {“href”: “https://panw-test.oktapreview.com/api/v1/meta/schemas/user/osc8zfz6plq7b0r830h7”}, “resetPassword”: {“href”: “https://panw-test.oktapreview.com/api/v1/users/00uujxnbh3uJw4tWA0h7/lifecycle/reset_password”, “method”: “POST”}, “reactivate”: {“href”: “https://panw-test.oktapreview.com/api/v1/users/00uujxnbh3uJw4tWA0h7/lifecycle/reactivate”, “method”: “POST”}, “self”: {“href”: “https://panw-test.oktapreview.com/api/v1/users/00uujxnbh3uJw4tWA0h7”}, “type”: {“href”: “https://panw-test.oktapreview.com/api/v1/meta/types/user/oty8zfz6plq7b0r830h7”}, “deactivate”: {“href”: “https://panw-test.oktapreview.com/api/v1/users/00uujxnbh3uJw4tWA0h7/lifecycle/deactivate”, “method”: “POST”}}

iam-update-user


Updates an existing user with the data passed in the user-profile argument.

Base Command

iam-update-user

Input

Argument Name Description Required
user-profile A User Profile indicator. Required
allow-enable When set to true, after the command execution the status of the user in the 3rd-party integration will be active. Optional

Context Output

Path Type Description
IAM.Vendor.active Boolean When true, indicates that the employee’s status is active in the 3rd-party integration.
IAM.Vendor.brand String Name of the integration.
IAM.Vendor.details string Provides the raw data from the 3rd-party integration.
IAM.Vendor.email String The employee’s email address.
IAM.Vendor.errorCode Number HTTP error response code.
IAM.Vendor.errorMessage String Reason why the API failed.
IAM.Vendor.id String The employee’s user ID in the app.
IAM.Vendor.instanceName string Name of the integration instance.
IAM.Vendor.success Boolean When true, indicates that the command was executed successfully.
IAM.Vendor.username String The employee’s username in the app.

Command Example

!iam-update-user user-profile={\"email\":\"testdemisto2@paloaltonetworks.com\", \"givenname\":\"Demisto-Test\"}

Human Readable Output

Update User Results (Okta IAM)
brand instanceName success active id username email details
Okta IAM Okta IAM_instance_1 true true 00uujxnbh3uJw4tWA0h7 testdemisto2@paloaltonetworks.com testdemisto2@paloaltonetworks.com id: 00uujxnbh3uJw4tWA0h7
status: PROVISIONED
created: 2020-10-18T17:54:30.000Z
activated: 2020-10-18T17:54:30.000Z
statusChanged: 2020-10-18T17:54:30.000Z
lastLogin: null
lastUpdated: 2020-10-18T17:56:53.000Z
passwordChanged: null
type: {“id”: “oty8zfz6plq7b0r830h7”}
profile: {“firstName”: “Demisto-Test”, “lastName”: “Test”, “mobilePhone”: null, “secondEmail”: null, “login”: “testdemisto2@paloaltonetworks.com”, “email”: “testdemisto2@paloaltonetworks.com”}
credentials: {“provider”: {“type”: “OKTA”, “name”: “OKTA”}}
_links: {“suspend”: {“href”: “https://panw-test.oktapreview.com/api/v1/users/00uujxnbh3uJw4tWA0h7/lifecycle/suspend”, “method”: “POST”}, “schema”: {“href”: “https://panw-test.oktapreview.com/api/v1/meta/schemas/user/osc8zfz6plq7b0r830h7”}, “resetPassword”: {“href”: “https://panw-test.oktapreview.com/api/v1/users/00uujxnbh3uJw4tWA0h7/lifecycle/reset_password”, “method”: “POST”}, “reactivate”: {“href”: “https://panw-test.oktapreview.com/api/v1/users/00uujxnbh3uJw4tWA0h7/lifecycle/reactivate”, “method”: “POST”}, “self”: {“href”: “https://panw-test.oktapreview.com/api/v1/users/00uujxnbh3uJw4tWA0h7”}, “type”: {“href”: “https://panw-test.oktapreview.com/api/v1/meta/types/user/oty8zfz6plq7b0r830h7”}, “deactivate”: {“href”: “https://panw-test.oktapreview.com/api/v1/users/00uujxnbh3uJw4tWA0h7/lifecycle/deactivate”, “method”: “POST”}}

iam-get-user


Retrieves a single user resource.

Base Command

iam-get-user

Input

Argument Name Description Required
user-profile A User Profile indicator. Required

Context Output

Path Type Description
IAM.Vendor.active Boolean When true, indicates that the employee’s status is active in the 3rd-party integration.
IAM.Vendor.brand String Name of the integration.
IAM.Vendor.details string Provides the raw data from the 3rd-party integration.
IAM.Vendor.email String The employee’s email address.
IAM.Vendor.errorCode Number HTTP error response code.
IAM.Vendor.errorMessage String Reason why the API failed.
IAM.Vendor.id String The employee’s user ID in the app.
IAM.Vendor.instanceName string Name of the integration instance.
IAM.Vendor.success Boolean When true, indicates that the command was executed successfully.
IAM.Vendor.username String The employee’s username in the app.

Command Example

!iam-get-user user-profile={\"email\":\"testdemisto2@paloaltonetworks.com\"}

Human Readable Output

Get User Results (Okta IAM)
brand instanceName success active id username email details
Okta IAM Okta IAM_instance_1 true true 00uujxnbh3uJw4tWA0h7 testdemisto2@paloaltonetworks.com testdemisto2@paloaltonetworks.com id: 00uujxnbh3uJw4tWA0h7
status: PROVISIONED
created: 2020-10-18T17:54:30.000Z
activated: 2020-10-18T17:54:30.000Z
statusChanged: 2020-10-18T17:54:30.000Z
lastLogin: null
lastUpdated: 2020-10-18T17:56:53.000Z
passwordChanged: null
type: {“id”: “oty8zfz6plq7b0r830h7”}
profile: {“firstName”: “Demisto-Test”, “lastName”: “Test”, “mobilePhone”: null, “secondEmail”: null, “login”: “testdemisto2@paloaltonetworks.com”, “email”: “testdemisto2@paloaltonetworks.com”}
credentials: {“provider”: {“type”: “OKTA”, “name”: “OKTA”}}
_links: {“suspend”: {“href”: “https://panw-test.oktapreview.com/api/v1/users/00uujxnbh3uJw4tWA0h7/lifecycle/suspend”, “method”: “POST”}, “schema”: {“href”: “https://panw-test.oktapreview.com/api/v1/meta/schemas/user/osc8zfz6plq7b0r830h7”}, “resetPassword”: {“href”: “https://panw-test.oktapreview.com/api/v1/users/00uujxnbh3uJw4tWA0h7/lifecycle/reset_password”, “method”: “POST”}, “reactivate”: {“href”: “https://panw-test.oktapreview.com/api/v1/users/00uujxnbh3uJw4tWA0h7/lifecycle/reactivate”, “method”: “POST”}, “self”: {“href”: “https://panw-test.oktapreview.com/api/v1/users/00uujxnbh3uJw4tWA0h7”}, “type”: {“href”: “https://panw-test.oktapreview.com/api/v1/meta/types/user/oty8zfz6plq7b0r830h7”}, “deactivate”: {“href”: “https://panw-test.oktapreview.com/api/v1/users/00uujxnbh3uJw4tWA0h7/lifecycle/deactivate”, “method”: “POST”}}

iam-disable-user


Disable an active user.

Base Command

iam-disable-user

Input

Argument Name Description Required
user-profile A User Profile indicator. Required

Context Output

Path Type Description
IAM.Vendor.active Boolean When true, indicates that the employee’s status is active in the 3rd-party integration.
IAM.Vendor.brand String Name of the integration.
IAM.Vendor.details string Provides the raw data from the 3rd-party integration.
IAM.Vendor.email String The employee’s email address.
IAM.Vendor.errorCode Number HTTP error response code.
IAM.Vendor.errorMessage String Reason why the API failed.
IAM.Vendor.id String The employee’s user ID in the app.
IAM.Vendor.instanceName string Name of the integration instance.
IAM.Vendor.success Boolean When true, indicates that the command was executed successfully.
IAM.Vendor.username String The employee’s username in the app.

Command Example

!iam-disable-user user-profile={\"email\":\"testdemisto2@paloaltonetworks.com\"}

Human Readable Output

Disable User Results (Okta IAM)
brand instanceName success active id username email details
Okta IAM Okta IAM_instance_1 true false 00uujxnbh3uJw4tWA0h7 testdemisto2@paloaltonetworks.com testdemisto2@paloaltonetworks.com id: 00uujxnbh3uJw4tWA0h7
status: PROVISIONED
created: 2020-10-18T17:54:30.000Z
activated: 2020-10-18T17:54:30.000Z
statusChanged: 2020-10-18T17:54:30.000Z
lastLogin: null
lastUpdated: 2020-10-18T17:56:53.000Z
passwordChanged: null
type: {“id”: “oty8zfz6plq7b0r830h7”}
profile: {“firstName”: “Demisto-Test”, “lastName”: “Test”, “mobilePhone”: null, “secondEmail”: null, “login”: “testdemisto2@paloaltonetworks.com”, “email”: “testdemisto2@paloaltonetworks.com”}
credentials: {“provider”: {“type”: “OKTA”, “name”: “OKTA”}}
_links: {“self”: {“href”: “https://panw-test.oktapreview.com/api/v1/users/00uujxnbh3uJw4tWA0h7”}}

okta-get-assigned-user-for-app


Gets a specific user assignment for an application by id.

Base Command

okta-get-app-user-assignment

Input

Argument Name Description Required
user_id ID of the user for which to get information. Required
application_id ID of the application for which to get information. Required

Context Output

Path Type Description
Okta.AppUserAssignment.UserID string ID of the user.
Okta.AppUserAssignment.AppID string ID of the application.
Okta.AppUserAssignment.IsAssigned boolean When True, indicates that the user is assigned to the application.
Okta.AppUserAssignment.ProfileInApp unknown The user profile data in the application.

Command Example

!okta-get-app-user-assignment user_id=00uuv6y8t1iy8YXm94h7 application_id=0oae3ioe51sQ64Aui2h7

Human Readable Output

App User Assignment
App ID Is Assigned User ID
0oae3ioe51sQ64Aui2h7 true 00uuv6y8t1iy8YXm94h7

okta-list-applications


Returns a list of Okta applications data.

Base Command

okta-iam-list-applications

Input

Argument Name Description Required
query Search for applications by their names. Optional
page Page number (0-based). Default is 0. Optional
limit Maximum number of apps to retrieve (maximal value is 200). Default is 50. Optional

Context Output

Path Type Description
Okta.Application.ID string ID of the application.
Okta.Application.Name string Name of the application.
Okta.Application.Label string Label of the application.
Okta.Application.Logo string Logo of the application.

Command Example

!okta-iam-list-applications limit=5 query="Workday"

Human Readable Output

Okta Applications (1 - 3)
ID Name Label Logo
0ob8zlypk6GVPRr2T0h7 workday Workday - Preview
0oabz0ozy5dDpEKyA0h7 workday Workday - Prod - DryRun
0oae3ioe51sQ64Aui2h7 workday Workday - Impl1

okta-list-user-applications


Returns a list of Okta applications data.

Base Command

okta-iam-list-user-applications

Input

Argument Name Description Required
user_id ID of the user for which to get the information. Required

Context Output

Path Type Description
Okta.Application.ID string ID of the application.
Okta.Application.Name string Name of the application.
Okta.Application.Label string Label of the application.
Okta.Application.Status string Status of the application.

Command Example

!okta-iam-list-user-applications user_id=00ux9v19bvTfQIjur0h7"

Human Readable Output

Okta User Applications
ID Name Label Status
0ob8zlypk6GVPRr2T0h7 active_directory pantest.local ACTIVE
0oabz0ozy5dDpEKyA0h7 test_app martsheet Test App ACTIVE

okta-iam-get-configuration


Gets the IAM configuration data from the integration context.

Base Command

okta-iam-get-configuration

Input

There are no input arguments for this command.

Context Output

Path Type Description
Okta.IAMConfiguration.ApplicationID String An Okta application ID.
Okta.IAMConfiguration.Name String Name of the Okta application.
Okta.IAMConfiguration.Label String Label of the Okta application.
Okta.IAMConfiguration.Logo String Logo of the application.
Okta.IAMConfiguration.Instance String An XSOAR IAM integration instance name.

Command Example

!okta-iam-get-configuration using="Okta IAM_instance_1_copy"

Human Readable Output

Okta IAM Configuration
ApplicationID Instance Label Logo Name
0oc8zlypk6GVPRr2G0h7 ServiceNow IAM_instance_1 ServiceNow servicenow

okta-iam-set-configuration


Updates IAM configuration data in the integration context.

Base Command

okta-iam-set-configuration

Input

Argument Name Description Required
configuration IAM configuration data. Required

Context Output

There is no context output for this command.

iam-get-group


Retrieves the group information, including its members.

Base Command

iam-get-group

Input

Argument Name Description Required
scim Group SCIM Data. Required
includeMembers Field to indicate if members need to be included in the response. . Possible values are: true, false. Default is true. Required

Context Output

Path Type Description
GetGroup.id String ID of the group.
GetGroup.displayName String The display name of the group.
GetGroup.members.display String The display name of the group member.
GetGroup.members.value String ID of the group member.
GetGroup.success Boolean Indicates whether the command succeeded.
GetGroup.errorCode Number HTTP error response code.
GetGroup.errorMessage String Reason why the API failed.

okta-get-logs


Gets logs by providing optional filters.

Base Command

okta-get-logs

Input

Argument Name Description Required
filter Useful for performing structured queries where constraints on LogEvent attribute values can be explicitly targeted. The following expressions are supported for events with the filter query parameter: eventType eq “ :eventType” -Events that have a specific action; eventType target.id eq “:id” - Events published with a specific target id; actor.id eq “:id”- Events published with a specific actor ID. For more information about filtering, visit https://developer.okta.com/docs/api/getting_started/design_principles#filtering. Optional
since Filters the lower time bound of the log events in the Internet Date/Time Format profile of ISO 8601. For example: 2017-05-03T16:22:18Z. Optional
until Filters the upper time bound of the log events in the Internet Date/Time Format profile of ISO 8601. For example: 2017-05-03T16:22:18Z. Optional
sortOrder The order of the returned events. Can be “ASCENDING” or “DESCENDING”. The default is “ASCENDING”. Possible values are: ASCENDING, DESCENDING. Default is ASCENDING. Optional
limit The maximum number of results to return. The default and maximum is 100. Optional

Context Output

Path Type Description
Okta.Logs.Events.actor.alternateId String Alternative ID of the actor.
Okta.Logs.Events.actor.displayName String Display name of the actor.
Okta.Logs.Events.actor.id String ID of the actor.
Okta.Logs.Events.client.userAgent.rawUserAgent String A raw string representation of user agent, formatted according to section 5.5.3 of HTTP/1.1 Semantics and Content. Both the browser and the OS fields can be derived from this field.
Okta.Logs.Events.client.userAgent.os String The operating system on which the client runs. For example, Microsoft Windows 10.
Okta.Logs.Events.client.userAgent.browser String Identifies the type of web browser, if relevant. For example, Chrome.
Okta.Logs.Events.client.device String Type of device from which the client operated. For example, Computer.
Okta.Logs.Events.client.id String For OAuth requests, the ID of the OAuth client making the request. For SSWS token requests, the ID of the agent making the request.
Okta.Logs.Events.client.ipAddress String IP address from which the client made its request.
Okta.Logs.Events.client.geographicalContext.city String The city encompassing the area containing the geo-location coordinates, if available. For example, Seattle, San Francisco.
Okta.Logs.Events.client.geographicalContext.state String Full name of the state or province encompassing the area containing the geo-location coordinates. For example, Montana, Incheon.
Okta.Logs.Events.client.geographicalContext.country String Full name of the country encompassing the area containing the geo-location coordinates. For example, France, Uganda.
Okta.Logs.Events.displayMessage String The display message for an event.
Okta.Logs.Events.eventType String Type of event that was published.
Okta.Logs.Events.outcome.result String Result of the action. Can be “SUCCESS”, “FAILURE”, “SKIPPED”, or “UNKNOWN”.
Okta.Logs.Events.outcome.reason String Reason for the result. For example, INVALID_CREDENTIALS.
Okta.Logs.Events.published String Timestamp when the event was published.
Okta.Logs.Events.severity String The event severity. Can be “DEBUG”, “INFO”, “WARN”, or “ERROR”.
Okta.Logs.Events.securityContext.asNumber Number Autonomous system number associated with the autonomous system that the event request was sourced to.
Okta.Logs.Events.securityContext.asOrg String Organization associated with the autonomous system that the event request was sourced to.
Okta.Logs.Events.securityContext.isp String Internet service provider used to send the event’s request.
Okta.Logs.Events.securityContext.domain String Specifies whether an event’s request is from a known proxy.
Okta.Logs.Events.request.ipChain.IP String IP address.
Okta.Logs.Events.request.ipChain.geographicalContext.city String The city encompassing the area containing the geo-location coordinates, if available. For example, Seattle, San Francisco.
Okta.Logs.Events.request.ipChain.geographicalContext.state String Full name of the state or province encompassing the area containing the geo-location coordinates. For example, Montana, Incheon.
Okta.Logs.Events.request.ipChain.geographicalContext.country String Full name of the country encompassing the area containing the geo-location coordinates. For example, France, Uganda.
Okta.Logs.Events.request.ipChain.source String Details regarding the source.
Okta.Logs.Events.target.id String ID of a target.
Okta.Logs.Events.target.type String Type of a target.
Okta.Logs.Events.target.alternateId String Alternative ID of a target.
Okta.Logs.Events.target.displayName String Display name of a target.

Configuration parameters

  • url — Okta URL (https://<domain>.okta.com) (required)
  • credentials
  • apitoken — API Token (See detailed instructions)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • create-user-enabled — Allow creating users
  • update-user-enabled — Allow updating users
  • enable-user-enabled — Allow enabling users
  • disable-user-enabled — Allow disabling users
  • create-if-not-exists — Automatically create user if not found in update command
  • mapper-in — Incoming Mapper (required)
  • mapper-out — Outgoing Mapper (required)
  • max_fetch — Fetch limit (recommended value is less than 200)
  • isFetch — Fetch incidents
  • incidentFetchInterval — Incidents Fetch Interval
  • incidentType — Incident type
  • auto_generate_query_filter — Query only application events configured in IAM Configuration
  • fetch_query_filter — Fetch Query Filter (Okta system log events)
  • first_fetch — First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days)

Commands (12)

  • get-mapping-fields

    Retrieves a User Profile schema which holds all of the user fields within the application. Used for outgoing-mapping through the Get Schema option.

  • iam-create-user

    Creates a user.

  • iam-disable-user

    Disable an active user.

  • iam-get-group

    Retrieves the group information, including its members.

  • iam-get-user

    Retrieves a single user resource.

  • iam-update-user

    Updates an existing user with the data passed in the user-profile argument.

  • okta-get-app-user-assignment

    Returns information about application's user assignment.

  • okta-get-logs

    Gets logs by providing optional filters.

  • okta-iam-get-configuration

    Gets the IAM configuration data from the integration context.

  • okta-iam-list-applications

    Returns a list of Okta applications data.

  • okta-iam-list-user-applications

    List all applications assigned to a user.

  • okta-iam-set-configuration

    Updates IAM configuration data in the integration context.

import demistomock as demisto  # noqa: F401
import urllib3
from CommonServerPython import *  # noqa: F401

# Disable insecure warnings
urllib3.disable_warnings()

"""CONSTANTS"""

BATCH_SIZE = 2000
DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
DEPROVISIONED_STATUS = "DEPROVISIONED"
USER_IS_DISABLED_MSG = "User is already disabled."
USER_IS_DISABLED_ERROR = "E0000007"
ERROR_CODES_TO_SKIP = [
    "E0000016",  # user is already enabled
    USER_IS_DISABLED_ERROR,
]
ERROR_CODES_TO_RETURN_ERROR = [
    "E0000047",  # rate limit - resets after 1 minute
]

FETCH_QUERY_EXCEPTION_MSG = (
    'If you marked the "Query only application events configured in IAM Configuration" '
    "checkbox in the instance configuration, you must add at least one application in "
    "the IAM Configuration incident before fetching logs from Okta. "
    "Alternatively, you can unmark this checkbox and provide a "
    '"Fetch Query Filter" parameter instead.'
)
GET_USER_ATTRIBUTES = ["id", "login", "email"]

MAX_LOGS_LIMIT = 1000

"""CLIENT CLASS"""


class Client(BaseClient):
    """
    Okta IAM Client class that implements logic to authenticate with Okta.
    """

    def test_connection(self):
        uri = "users/me"
        self._http_request(method="GET", url_suffix=uri)

    def get_user(self, filter_name: str, filter_value: str):
        filter_name = filter_name if filter_name == "id" else f"profile.{filter_name}"
        uri = "users"
        query_params = {"filter": f'{filter_name} eq "{filter_value}"'}

        res = self._http_request(method="GET", url_suffix=uri, params=query_params)

        if res and len(res) == 1:
            return res[0]
        return None

    def deactivate_user(self, user_id):
        uri = f"users/{user_id}/lifecycle/deactivate"
        self._http_request(method="POST", url_suffix=uri)

    def activate_user(self, user_id):
        query_params = {"sendEmail": "false"}
        uri = f"users/{user_id}/lifecycle/activate"
        self._http_request(method="POST", url_suffix=uri, params=query_params)

    def create_user(self, user_data):
        # create a user in staged mode (not active)
        body = {"profile": user_data}
        uri = "users"
        query_params = {"activate": "false", "provider": "true"}
        res = self._http_request(method="POST", url_suffix=uri, json_data=body, params=query_params)
        return res

    def update_user(self, user_id, user_data):
        body = {"profile": user_data}
        uri = f"users/{user_id}"
        res = self._http_request(method="POST", url_suffix=uri, json_data=body)
        return res

    def get_okta_fields(self):
        okta_fields = {}
        uri = "meta/schemas/user/default"
        res = self._http_request(method="GET", url_suffix=uri)

        base_properties = res.get("definitions", {}).get("base", {}).get("properties", {})
        okta_fields.update({k: base_properties[k].get("title") for k in base_properties})

        custom_properties = res.get("definitions", {}).get("custom", {}).get("properties", {})
        okta_fields.update({k: custom_properties[k].get("title") for k in custom_properties})

        return okta_fields

    def http_request(self, method, url_suffix, full_url=None, params=None, data=None, headers=None):
        if headers is None:
            headers = self._headers
        full_url = full_url if full_url else urljoin(self._base_url, url_suffix)

        res = requests.request(method, full_url, verify=self._verify, headers=headers, params=params, json=data)
        return res

    def search_group(self, group_name):
        uri = "groups"
        query_params = {"q": encode_string_results(group_name)}
        return self.http_request(method="GET", url_suffix=uri, params=query_params)

    def get_group_by_id(self, group_id):
        uri = f"groups/{group_id}"
        return self.http_request(method="GET", url_suffix=uri)

    def get_group_members(self, group_id):
        uri = f"groups/{group_id}/users"
        return self.get_paged_results(uri)

    def get_paged_results(self, uri, query_param=None):
        response = self.http_request(method="GET", url_suffix=uri, params=query_param)
        paged_results = response.json()
        if response.status_code != 200:
            raise Exception(f"Error occurred while calling Okta API: {response.request.url}. Response: {response.json()}")
        while "next" in response.links and len(response.json()) > 0:
            next_page = response.links.get("next").get("url")
            response = self._http_request(method="GET", full_url=next_page, url_suffix="")
            if response.status_code != 200:
                raise Exception(f"Error occurred while calling Okta API: {response.request.url}. Response: {response.json()}")
            paged_results += response.json()
        return paged_results

    def get_app_user_assignment(self, application_id, user_id):
        uri = f"/apps/{application_id}/users/{user_id}"
        res = self._http_request(method="GET", url_suffix=uri, resp_type="response", ok_codes=(200, 404))
        return res

    def list_user_apps(self, user_id):
        uri = "apps"
        query_params = {"filter": f'user.id eq "{user_id}"'}
        res = self._http_request(method="GET", url_suffix=uri, params=query_params)
        return res

    def list_apps(self, query, page, limit):
        query_params = {"q": query, "limit": limit}

        curr_page = 0
        apps_batch, next_page = self.list_apps_batch(url_suffix="/apps", params=query_params)

        while apps_batch and curr_page != page:
            curr_page += 1
            apps_batch, next_page = self.list_apps_batch(full_url=next_page)

        if not apps_batch:
            apps_batch = []
        return apps_batch

    def list_apps_batch(self, url_suffix="", params=None, full_url=""):
        """Gets a batch of apps from Okta.
        Args:
            url_suffix (str): The apps API endpoint.
            params (dict): The API query params.
            full_url (str): The full url retrieved from the last API call.

        Return:
            apps_batch (dict): The logs batch.
            next_page (str): URL for next API call (equals '' on last batch).
        """
        if not url_suffix and not full_url:
            return None, None

        res = self._http_request(method="GET", url_suffix=url_suffix, params=params, full_url=full_url, resp_type="response")

        logs_batch = res.json()
        next_page = res.links.get("next", {}).get("url")

        return logs_batch, next_page

    def get_logs(
        self,
        next_page=None,
        last_run_time=None,
        time_now=None,
        query_filter=None,
        auto_generate_filter=False,
        context=None,
        limit=None,
    ):
        logs = []

        uri = "logs"

        if auto_generate_filter:
            query_filter = get_query_filter(context)

        params = {"filter": query_filter, "since": last_run_time, "until": time_now, "limit": limit}
        limit = int(limit) if limit else None
        if limit and limit <= MAX_LOGS_LIMIT:
            return self._http_request(
                method="GET",
                url_suffix=uri,
                params=params,
            ), None

        if limit and limit > MAX_LOGS_LIMIT:
            params["limit"] = MAX_LOGS_LIMIT

        logs_batch, next_page = self.get_logs_batch(url_suffix=uri, params=params, full_url=next_page)

        try:
            while logs_batch:
                logs.extend(logs_batch)
                if limit and len(logs) > limit:
                    return logs[:limit], next_page
                logs_batch, next_page = self.get_logs_batch(full_url=next_page)
        except DemistoException as e:
            # in case of too many API calls, we return what we got and save the next_page for next fetch
            if not is_rate_limit_error(e):
                raise e

        return logs, next_page

    def get_logs_batch(self, url_suffix="", params=None, full_url=""):
        """Gets a batch of logs from Okta.
        Args:
            url_suffix (str): The logs API endpoint.
            params (dict): The API query params.
            full_url (str): The full url retrieved from the last API call. Preferred over url_suffix if not empty.

        Return:
            logs_batch (dict): The logs batch.
            next_page (str): URL for next API call (equals '' on last batch).
        """
        if not url_suffix and not full_url:
            return None, None

        res = self._http_request(method="GET", url_suffix=url_suffix, params=params, full_url=full_url, resp_type="response")

        logs_batch = res.json()
        next_page = res.links.get("next", {}).get("url")

        return logs_batch, next_page


"""HELPER FUNCTIONS"""


def get_all_user_profiles():
    query = 'type:"User Profile"'
    email_to_user_profile = {}

    user_profiles: List[dict] = []
    search_indicators = IndicatorsSearcher(query=query, size=BATCH_SIZE)
    for user_profile_res in search_indicators:
        user_profiles.extend(user_profile_res.get("iocs") or [])

    for user_profile in user_profiles:
        user_profile = user_profile.get("CustomFields", {})
        email_to_user_profile[user_profile.get("email")] = user_profile

    return email_to_user_profile


def get_event_username(log_entry):
    for target in log_entry.get("target", []):
        if target.get("type") == "User":
            return target.get("alternateId")
    return None


def should_drop_event(log_entry, email_to_user_profile):
    """Returns a boolean value indicates whether the incident should be dropped.

    Args:
        log_entry (dict): The log entry.

    Returns:
        (bool) True iff the event should be dropped.
    """
    username = get_event_username(log_entry)
    if username is not None and email_to_user_profile.get(username) is None:
        demisto.info(f"Dropping incident for user with username {username} - User Profile does not exist in XSOAR.")
        return True
    return False


def add_user_profile_data_to_entry(log_entry, email_to_user_profile):
    username = get_event_username(log_entry)
    user_profile = email_to_user_profile.get(username, {})
    log_entry.update(user_profile)
    log_entry["UserProfile"] = user_profile


def get_query_filter(context):
    iam_configuration = context.get("IAMConfiguration", [])
    if not iam_configuration:
        raise DemistoException(FETCH_QUERY_EXCEPTION_MSG)

    application_ids = [row["ApplicationID"] for row in iam_configuration]

    query_filter = '(eventType eq "application.user_membership.add" or eventType eq "application.user_membership.remove") and'

    query_filter += "(" + " or ".join([f'target.id co "{app_id}"' for app_id in application_ids]) + ")"

    return query_filter


def is_rate_limit_error(e):
    if hasattr(e, "res") and e.res is not None:
        return e.res.status_code == 429
    return False


def handle_exception(user_profile, e, action, okta_user=None):
    """Handles failed responses from Okta API by setting the User Profile object with the results.

    Args:
        user_profile (IAMUserProfile): The User Profile object.
        e (Exception): The exception error. If DemistoException, holds the response json.
        action (IAMActions): An enum represents the current action (get, update, create, etc).
    """
    if e.__class__ is DemistoException and hasattr(e, "res") and e.res is not None:
        try:
            resp = e.res.json()
            error_code = resp.get("errorCode")
            error_message = get_error_details(resp)
        except ValueError:
            error_code = e.res.status_code
            error_message = str(e)
    else:
        error_code = ""
        error_message = str(e)

    if error_code == USER_IS_DISABLED_ERROR:
        user_profile.set_user_is_already_disabled(okta_user)

    elif error_code in ERROR_CODES_TO_SKIP:
        user_profile.set_result(action=action, skip=True, skip_reason=error_message)
    else:
        should_return_error = error_code in ERROR_CODES_TO_RETURN_ERROR
        user_profile.set_result(
            action=action, success=False, return_error=should_return_error, error_code=error_code, error_message=error_message
        )

    demisto.error(traceback.format_exc())


def get_error_details(res):
    """Parses the error details retrieved from Okta and outputs the resulted string.

    Args:
        res (dict): The data retrieved from Okta.

    Returns:
        (str) The parsed error details.
    """
    error_msg = f'{res.get("errorSummary")}. '
    causes = ""
    for idx, cause in enumerate(res.get("errorCauses", []), 1):
        causes += f'{idx}. {cause.get("errorSummary")}\n'
    if causes:
        error_msg += f"Reason:\n{causes}"
    return error_msg


"""COMMAND FUNCTIONS"""


def test_module(client, is_fetch, fetch_query_filter, auto_generate_query_filter, context, first_fetch_str):
    if is_fetch:
        if auto_generate_query_filter:
            get_query_filter(context)  # will raise an exception if configuration doesn't exist
        elif not fetch_query_filter:
            raise DemistoException(FETCH_QUERY_EXCEPTION_MSG)
    try:
        dateparser.parse(first_fetch_str).strftime(DATE_FORMAT)  # type: ignore
    except AttributeError:
        raise DemistoException("First fetch timestamp parameter is not in the correct format.")

    client.test_connection()

    return_results("ok")


def get_mapping_fields_command(client):
    okta_fields = client.get_okta_fields()
    incident_type_scheme = SchemeTypeMapping(type_name=IAMUserProfile.DEFAULT_INCIDENT_TYPE)

    for field, description in okta_fields.items():
        incident_type_scheme.add_field(field, description)

    return GetMappingFieldsResponse([incident_type_scheme])


def get_user_command(client, args, mapper_in, mapper_out):
    user_profile = IAMUserProfile(
        user_profile=args.get("user-profile"), mapper=mapper_out, incident_type=IAMUserProfile.UPDATE_INCIDENT_TYPE
    )
    try:
        iam_attr, iam_attr_value = user_profile.get_first_available_iam_user_attr(GET_USER_ATTRIBUTES)
        okta_user = client.get_user(iam_attr, iam_attr_value)
        if not okta_user:
            error_code, error_message = IAMErrors.USER_DOES_NOT_EXIST
            user_profile.set_result(action=IAMActions.GET_USER, success=False, error_code=error_code, error_message=error_message)
        else:
            user_profile.update_with_app_data(okta_user, mapper_in)
            user_profile.set_result(
                action=IAMActions.GET_USER,
                success=True,
                active=okta_user.get("status") != DEPROVISIONED_STATUS,
                iden=okta_user.get("id"),
                email=okta_user.get("profile", {}).get("email"),
                username=okta_user.get("profile", {}).get("login"),
                details=okta_user,
            )

    except Exception as e:
        handle_exception(user_profile, e, IAMActions.GET_USER)

    return user_profile


def disable_user_command(client, args, is_command_enabled, mapper_out):
    user_profile = IAMUserProfile(
        user_profile=args.get("user-profile"), mapper=mapper_out, incident_type=IAMUserProfile.UPDATE_INCIDENT_TYPE
    )
    okta_user = None
    if not is_command_enabled:
        user_profile.set_result(action=IAMActions.DISABLE_USER, skip=True, skip_reason="Command is disabled.")
    else:
        try:
            iam_attr, iam_attr_value = user_profile.get_first_available_iam_user_attr(GET_USER_ATTRIBUTES)
            okta_user = client.get_user(iam_attr, iam_attr_value)
            if not okta_user:
                _, error_message = IAMErrors.USER_DOES_NOT_EXIST
                user_profile.set_result(action=IAMActions.DISABLE_USER, skip=True, skip_reason=error_message)
            else:
                client.deactivate_user(okta_user.get("id"))
                user_profile.set_result(
                    action=IAMActions.DISABLE_USER,
                    success=True,
                    active=False,
                    iden=okta_user.get("id"),
                    email=okta_user.get("profile", {}).get("email"),
                    username=okta_user.get("profile", {}).get("login"),
                    details=okta_user,
                )

        except Exception as e:
            handle_exception(user_profile, e, IAMActions.DISABLE_USER, okta_user)

    return user_profile


def create_user_command(client, args, mapper_out, is_command_enabled, is_update_user_enabled, is_enable_enabled):
    user_profile = IAMUserProfile(
        user_profile=args.get("user-profile"), mapper=mapper_out, incident_type=IAMUserProfile.CREATE_INCIDENT_TYPE
    )
    if not is_command_enabled:
        user_profile.set_result(action=IAMActions.CREATE_USER, skip=True, skip_reason="Command is disabled.")
    else:
        try:
            iam_attr, iam_attr_value = user_profile.get_first_available_iam_user_attr(GET_USER_ATTRIBUTES)
            okta_user = client.get_user(iam_attr, iam_attr_value)
            if okta_user:
                # if user exists, update its data
                return update_user_command(
                    client,
                    args,
                    mapper_out,
                    is_update_user_enabled,
                    is_enable_enabled,
                    is_create_user_enabled=False,
                    create_if_not_exists=False,
                )
            else:
                okta_profile = user_profile.map_object(mapper_out, incident_type=IAMUserProfile.CREATE_INCIDENT_TYPE)
                created_user = client.create_user(okta_profile)
                client.activate_user(created_user.get("id"))
                user_profile.set_result(
                    action=IAMActions.CREATE_USER,
                    success=True,
                    active=created_user.get("status") != DEPROVISIONED_STATUS,
                    iden=created_user.get("id"),
                    email=created_user.get("profile", {}).get("email"),
                    username=created_user.get("profile", {}).get("login"),
                    details=created_user,
                )

        except Exception as e:
            handle_exception(user_profile, e, IAMActions.CREATE_USER)

    return user_profile


def update_user_command(
    client, args, mapper_out, is_command_enabled, is_enable_enabled, is_create_user_enabled, create_if_not_exists
):
    user_profile = IAMUserProfile(
        user_profile=args.get("user-profile"), mapper=mapper_out, incident_type=IAMUserProfile.UPDATE_INCIDENT_TYPE
    )
    allow_enable = args.get("allow-enable") == "true"
    if not is_command_enabled:
        user_profile.set_result(action=IAMActions.UPDATE_USER, skip=True, skip_reason="Command is disabled.")
    else:
        try:
            iam_attr, iam_attr_value = user_profile.get_first_available_iam_user_attr(GET_USER_ATTRIBUTES, use_old_user_data=True)
            okta_user = client.get_user(iam_attr, iam_attr_value)
            if okta_user:
                user_id = okta_user.get("id")

                if allow_enable and is_enable_enabled and okta_user.get("status") == DEPROVISIONED_STATUS:
                    client.activate_user(user_id)
                    user_profile.set_result(
                        action=IAMActions.ENABLE_USER,
                        success=True,
                        active=True,
                        iden=okta_user.get("id"),
                        email=okta_user.get("profile", {}).get("email"),
                        username=okta_user.get("profile", {}).get("login"),
                        details=okta_user,
                    )
                else:
                    okta_profile = user_profile.map_object(mapper_out, incident_type=IAMUserProfile.UPDATE_INCIDENT_TYPE)
                    updated_user = client.update_user(user_id, okta_profile)
                    user_profile.set_result(
                        action=IAMActions.UPDATE_USER,
                        success=True,
                        active=okta_user.get("status") != DEPROVISIONED_STATUS,
                        iden=updated_user.get("id"),
                        email=updated_user.get("profile", {}).get("email"),
                        username=updated_user.get("profile", {}).get("login"),
                        details=updated_user,
                    )
            else:
                if create_if_not_exists:
                    return create_user_command(client, args, mapper_out, is_create_user_enabled, False, False)
                else:
                    _, error_message = IAMErrors.USER_DOES_NOT_EXIST
                    user_profile.set_result(action=IAMActions.UPDATE_USER, skip=True, skip_reason=error_message)

        except Exception as e:
            handle_exception(user_profile, e, IAMActions.UPDATE_USER)

    return user_profile


def get_app_user_assignment_command(client, args):
    user_id = args.get("user_id")
    application_id = args.get("application_id")

    res = client.get_app_user_assignment(application_id, user_id)
    raw_response = res.json()

    is_user_assigned_to_app = res.status_code == 200

    outputs = {"UserID": user_id, "AppID": application_id, "IsAssigned": is_user_assigned_to_app}

    readable_output = tableToMarkdown(
        "App User Assignment", outputs, headers=["UserID", "AppID", "IsAssigned"], headerTransform=pascalToSpace
    )

    if is_user_assigned_to_app:
        outputs["ProfileInApp"] = raw_response.get("profile")
        profile_readable = tableToMarkdown("Profile in App", raw_response.get("profile"), removeNull=True)
        readable_output += f"\n{profile_readable}"

    return CommandResults(
        outputs=outputs,
        outputs_prefix="Okta.AppUserAssignment",
        outputs_key_field=["UserID", "AppID"],
        readable_output=readable_output,
        raw_response=raw_response,
    )


def list_apps_command(client, args):
    query = args.get("query")
    page = int(args.get("page"))
    limit = min(int(args.get("limit")), 200)

    applications = client.list_apps(query, page, limit)

    outputs = []

    for app in applications:
        outputs.append(
            {
                "ID": app.get("id"),
                "Name": app.get("name"),
                "Label": app.get("label"),
                "Logo": f"![]({app.get('_links', {}).get('logo', [{}])[0].get('href')})",
            }
        )

    title = "Okta Applications"

    if applications:
        from_idx = page * limit + 1
        to_idx = from_idx + len(applications) - 1
        title += f" ({from_idx} - {to_idx})"

    return CommandResults(
        outputs=outputs,
        outputs_prefix="Okta.Application",
        outputs_key_field="ID",
        readable_output=tableToMarkdown(title, outputs, headers=["ID", "Name", "Label", "Logo"]),
    )


def list_user_apps_command(client, args):
    user_id = args.get("user_id")
    applications = client.list_user_apps(user_id)
    outputs = []

    for app in applications:
        outputs.append({"ID": app.get("id"), "Name": app.get("name"), "Label": app.get("label"), "Status": app.get("status")})

    title = "Okta User Applications"
    return CommandResults(
        outputs=outputs,
        outputs_prefix="Okta.Application",
        outputs_key_field="ID",
        readable_output=tableToMarkdown(title, outputs, headers=["ID", "Name", "Label", "Status"]),
    )


def get_configuration(context):
    iam_configuration = context.get("IAMConfiguration", [])

    return CommandResults(
        outputs=iam_configuration,
        outputs_prefix="Okta.IAMConfiguration",
        outputs_key_field="ApplicationID",
        readable_output=tableToMarkdown("Okta IAM Configuration", iam_configuration),
    )


def set_configuration(args):
    iam_configuration = json.loads(args.get("configuration"))
    context = {"IAMConfiguration": iam_configuration}
    return context


def fetch_incidents(client, last_run, first_fetch_str, fetch_limit, query_filter=None, auto_generate_filter=False, context=None):
    """If no events were saved from last run, returns new events from Okta's /log API. Otherwise,
    returns the events from last run. In both cases, no more than `fetch_limit` incidents will be returned,
    and the rest of them will be saved for next run.

        Args:
            client: (BaseClient) Okta client.
            last_run: (dict) The "last run" object that was set on the previous run.
            first_fetch_str: (str) First fetch time parameter (e.g. "1 day", "2 months", etc).
            fetch_limit: (int) Maximum number of incidents to return.
            query_filter: (str) Logs API query filter.
            auto_generate_filter: (bool) Whether or not to automatically generate the query filter.
            context: (dict) Integration Context object.
        Returns:
            incidents: (dict) Incidents/events that will be created in Cortex XSOAR
            next_run: (dict) The "last run" object for the next run.
    """

    incidents = last_run.get("incidents", [])
    last_run_full_url = last_run.get("last_run_full_url")

    first_fetch_date = dateparser.parse(first_fetch_str)
    assert first_fetch_date is not None, f"could not parse {first_fetch_str}"
    first_fetch = first_fetch_date.strftime(DATE_FORMAT)
    last_run_time = last_run.get("last_run_time", first_fetch)  # if last_run_time is undefined, use first_fetch
    time_now = datetime.now().strftime(DATE_FORMAT)

    demisto.debug(f"Okta: Fetching logs from {last_run_time} to {time_now}.")
    if not incidents:
        email_to_user_profile = get_all_user_profiles()

        log_events, last_run_full_url = client.get_logs(
            last_run_full_url, last_run_time, time_now, query_filter, auto_generate_filter, context
        )
        for entry in log_events:
            if not should_drop_event(entry, email_to_user_profile):
                add_user_profile_data_to_entry(entry, email_to_user_profile)
                incident = {"rawJSON": json.dumps(entry)}
                incidents.append(incident)

    next_run = {"incidents": incidents[fetch_limit:], "last_run_time": time_now, "last_run_full_url": last_run_full_url}

    return incidents[:fetch_limit], next_run


class OutputContext:
    """
    Class to build a generic output and context.
    """

    def __init__(
        self,
        success=None,
        active=None,
        id=None,
        username=None,
        email=None,
        errorCode=None,
        errorMessage=None,
        details=None,
        displayName=None,
        members=None,
    ):
        self.instanceName = demisto.callingContext["context"]["IntegrationInstance"]
        self.brand = demisto.callingContext["context"]["IntegrationBrand"]
        self.command = demisto.command().replace("-", "_").title().replace("_", "")
        self.success = success
        self.active = active
        self.id = id
        self.username = username
        self.email = email
        self.errorCode = errorCode
        self.errorMessage = errorMessage
        self.details = details
        self.displayName = displayName  # Used in group
        self.members = members  # Used in group
        self.data = {
            "brand": self.brand,
            "instanceName": self.instanceName,
            "success": success,
            "active": active,
            "id": id,
            "username": username,
            "email": email,
            "errorCode": errorCode,
            "errorMessage": errorMessage,
            "details": details,
            "displayName": displayName,
            "members": members,
        }
        # Remoove empty values
        self.data = {k: v for k, v in self.data.items() if v is not None}


def get_group_command(client, args):
    scim = safe_load_json(args.get("scim"))
    group_id = scim.get("id")
    group_name = scim.get("displayName")

    if not (group_id or group_name):
        return_error("You must supply either 'id' or 'displayName' in the scim data")

    group_search_result = None
    if not group_id:
        res = client.search_group(group_name)
        res_json = res.json()

        if res.status_code == 200:
            if len(res_json) < 1:
                generic_iam_context = OutputContext(
                    success=False, displayName=group_name, errorCode=404, errorMessage="Group Not Found", details=res_json
                )
            else:
                generic_iam_context = OutputContext()
                group_search_result = res_json
        else:
            generic_iam_context = OutputContext(
                success=False,
                displayName=group_name,
                id=group_id,
                errorCode=res_json.get("errorCode"),
                errorMessage=res_json.get("errorSummary"),
                details=res_json,
            )

        if not group_search_result:
            return CommandResults(
                raw_response=generic_iam_context.data,
                outputs_prefix=generic_iam_context.command,
                outputs_key_field="id",
                outputs=generic_iam_context.data,
                readable_output=tableToMarkdown("Okta Get Group:", generic_iam_context.data, removeNull=True),
            )

    if group_search_result and len(group_search_result) > 1:
        generic_iam_context_data_list = []

        for group in group_search_result:
            group_name = group.get("profile", {}).get("name")
            generic_iam_context = OutputContext(success=True, id=group.get("id"), displayName=group_name)
            generic_iam_context_data_list.append(generic_iam_context.data)

        return CommandResults(
            raw_response=generic_iam_context_data_list,
            outputs_prefix=generic_iam_context.command,
            outputs_key_field="id",
            outputs=generic_iam_context_data_list,
            readable_output=tableToMarkdown("Okta Get Group:", generic_iam_context_data_list, removeNull=True),
        )
    elif not group_id and isinstance(group_search_result, list):
        group_id = group_search_result[0].get("id")

    res = client.get_group_by_id(group_id)
    res_json = res.json()
    if res.status_code == 200:
        group_member_profiles = []
        include_members = args.get("includeMembers")
        if include_members.lower() == "true":
            group_members = client.get_group_members(group_id)
            for member in group_members:
                if member.get("status") != DEPROVISIONED_STATUS:
                    profile = member.get("profile", {})
                    group_member_profile = {"value": member.get("id"), "display": profile.get("login")}
                    group_member_profiles.append(group_member_profile)
        generic_iam_context = OutputContext(
            success=True,
            id=res_json.get("id"),
            displayName=res_json.get("profile", {}).get("name"),
            members=group_member_profiles,
        )
    elif res.status_code == 404:
        generic_iam_context = OutputContext(
            success=False, displayName=group_name, id=group_id, errorCode=404, errorMessage="Group Not Found", details=res_json
        )
    else:
        generic_iam_context = OutputContext(
            success=False,
            displayName=group_name,
            id=group_id,
            errorCode=res_json.get("errorCode"),
            errorMessage=res_json.get("errorSummary"),
            details=res_json,
        )

    return CommandResults(
        raw_response=generic_iam_context.data,
        outputs_prefix=generic_iam_context.command,
        outputs_key_field="id",
        outputs=generic_iam_context.data,
        readable_output=tableToMarkdown("Okta Get Group:", generic_iam_context.data, removeNull=True),
    )


def get_logs_command(client, args):
    filter = args.get("filter")
    since = args.get("since")
    until = args.get("until")
    limit = args.get("limit")
    log_events, _ = client.get_logs(query_filter=filter, last_run_time=since, time_now=until, limit=limit)

    if not log_events:
        return CommandResults(readable_output="No logs found.", outputs={}, raw_response=log_events)

    return CommandResults(
        raw_response=log_events,
        outputs_prefix="Okta.Logs.Events",
        outputs_key_field="uuid",
        outputs=log_events,
        readable_output=tableToMarkdown("Okta Log Events:", log_events),
    )


def main():
    user_profile = None
    params = demisto.params()
    base_url = urljoin(params["url"].strip("/"), "/api/v1/")
    token = params.get("credentials", {}).get("password", "") or params.get("apitoken", "")

    if not token:
        raise ValueError("Missing API token.")

    mapper_in = params.get("mapper-in")
    mapper_out = params.get("mapper-out")
    verify_certificate = not params.get("insecure", False)
    proxy = params.get("proxy", False)
    command = demisto.command()
    args = demisto.args()

    is_create_enabled = params.get("create-user-enabled")
    is_enable_enabled = params.get("enable-user-enabled")
    is_disable_enabled = params.get("disable-user-enabled")
    is_update_enabled = demisto.params().get("update-user-enabled")
    create_if_not_exists = demisto.params().get("create-if-not-exists")

    is_fetch = params.get("isFetch")
    first_fetch_str = params.get("first_fetch")
    fetch_limit = int(params.get("max_fetch", 1))
    auto_generate_query_filter = params.get("auto_generate_query_filter")
    fetch_query_filter = params.get("fetch_query_filter")
    context = demisto.getIntegrationContext()

    headers = {"Content-Type": "application/json", "Accept": "application/json", "Authorization": f"SSWS {token}"}

    client = Client(base_url=base_url, verify=verify_certificate, proxy=proxy, headers=headers, ok_codes=(200,))

    demisto.debug(f"Command being called is {command}")

    if command == "iam-get-user":
        user_profile = get_user_command(client, args, mapper_in, mapper_out)

    elif command == "iam-create-user":
        user_profile = create_user_command(client, args, mapper_out, is_create_enabled, is_update_enabled, is_enable_enabled)

    elif command == "iam-update-user":
        user_profile = update_user_command(
            client, args, mapper_out, is_update_enabled, is_enable_enabled, is_create_enabled, create_if_not_exists
        )

    elif command == "iam-disable-user":
        user_profile = disable_user_command(client, args, is_disable_enabled, mapper_out)

    if user_profile:
        return_results(user_profile)

    try:
        if command == "test-module":
            test_module(client, is_fetch, fetch_query_filter, auto_generate_query_filter, context, first_fetch_str)

        elif command == "get-mapping-fields":
            return_results(get_mapping_fields_command(client))

        elif command == "okta-get-app-user-assignment":
            return_results(get_app_user_assignment_command(client, args))

        elif command == "okta-iam-list-applications":
            return_results(list_apps_command(client, args))

        elif command == "okta-iam-list-user-applications":
            return_results(list_user_apps_command(client, args))

        elif command == "okta-iam-get-configuration":
            return_results(get_configuration(context))

        elif command == "okta-iam-set-configuration":
            context = set_configuration(args)
            demisto.setIntegrationContext(context)

        elif command == "iam-get-group":
            return_results(get_group_command(client, args))

        elif command == "okta-get-logs":
            return_results(get_logs_command(client, args))

        elif command == "fetch-incidents":
            last_run = demisto.getLastRun()
            context = demisto.getIntegrationContext()
            incidents, next_run = fetch_incidents(
                client, last_run, first_fetch_str, fetch_limit, fetch_query_filter, auto_generate_query_filter, context
            )
            demisto.incidents(incidents)
            demisto.setLastRun(next_run)

    except Exception as e:
        # For any other integration command exception, return an error
        return_error(f"Failed to execute {command} command. Error: {e!s}")


from IAMApiModule import *  # noqa: E402

if __name__ in ("__main__", "__builtin__", "builtins"):
    main()