Active Directory Query v2

The Active Directory Query integration enables you to access and manage Active Directory objects (users, contacts, and computers).

Authentication & Identity Management · Active Directory Query

Details

IDActive Directory Query v2
ProviderMicrosoft
CategoryAuthentication & Identity Management
From Version5.0.0
Docker Imagedemisto/ldap:2.9.1.9062583
Supported ModulesAgentix Cloud Runtime Security Cloud Posture Security XSIAM EDR Cortex Cloud

README

The Active Directory Query integration enables you to access and manage Active Directory objects (users, contacts, and computers).
This integration was integrated and tested with version 1.5.0 of Active Directory Query v2

Some changes have been made that might affect your existing content.
If you are upgrading from a previous version of this integration, see Breaking Changes.

Use Cases

Query for Active Directory objects
  • Use the !ad-search command to run a query for Active Directory objects (users, contacts, computers, and so on). This command enables you to determine which data fields should be returned for the objects.
Manage users and contacts
  • The integration enables you to create, update, and delete users and contacts in Active Directory using the following commands:
    • ad-create-user
    • ad-create-contact
    • ad-update-user
    • ad-update-contact
    • ad-delete-user (to delete both users and contacts)
  • Add or remove users from groups using the following commands:
    • ad-add-to-group
    • ad-remove-from-group
  • Enable or disable a user account using the following commands:
    • ad-enable-account
    • ad-disable-user-account
Manage Computers
  • Modify a computer organizational unit using the ‘ad-modify-computer-ou’ command.
  • Add or remove a computer from a group using the following commands:
    • ad-add-to-group
    • ad-remove-from-group
IAM premium pack uses
  • Create or modify Active Directory users.
  • Manage user accounts and their status

Configure Active Directory Query v2 in Cortex

Parameter Description Required
Server IP address (for example, 192.168.0.1) The Server IP that should be used to access Active Directory. True
Port Server port. If not specified, the default port is 389 for LDAP, 636 for LDAPS, or 3268 for global catalog servers. False
Credentials User credentials. True
Password   True
NTLM authentication Indicates whether to use NTLM authentication. False
Base DN (for example “dc=company,dc=com”) The basic hierarchical path of objects in the active directory. True
Page size The number of results to be returned, per page (page - response content from AD server), from a query. This may effect query run time. True
Secure Connection Use SSL or Start TLS for secure connection or ‘None’ for communication over clear-text. True
SSL Version The SSL\TLS version to use in SSL or Start TLS connections types. It is recommended to select the TLS_CLIENT option, which auto-negotiate the highest protocol version that both the client and server support, and configure the context client-side connections. For more information please see: ssl.PROTOCOLS). False
Trust any certificate (not secure) Select to avoid server certification validation. You may want to do this in case Cortex XSOAR cannot validate the integration server certificate (due to a missing CA certificate) False
Incoming Mapper Used in the IAM commands. True
Outgoing Mapper Used in the IAM commands. True
Group CN for terminated employees   False
Create user if does not exist If true, the user is created if the user profile doesn’t exist in AD. Used in IAM commands only. False

Note: For queries and operations across multiple domains within an Active Directory forest the server port should be 3268. This port is used for queries specifically targeted for the global catalog. LDAP requests sent to port 3268 can be used to search for objects in the entire Active Directory forest. For more information on global catalog see the Microsoft documentation.

Identity Lifecycle Management premium pack configuration

The premium ILM content pack introduces new functionality that uses both an incoming and an outgoing mapper.

  1. Configure the “Incoming Mapper” with the name of the incoming mapper that you’re using. ILM’s default mapper is “User Profile - Active Directory (Incoming)”.
  2. Configure the “Outgoing Mapper” with the name of the outgoing mapper that you’re using. ILM’s default mapper is “User Profile - Active Directory (Outgoing)”.

Note: As part of the configuration of the mapper, you must map a value to the OU (organizational unit) required field. To do this, create a transformer that maps a user attribute of your choice to an OU value.

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.

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.

ad-expire-password


Expires the password of an Active Directory user.

Base Command

ad-expire-password

Required Permissions

Requires Reset user passwords and force password change at next logon permissions.

Input

Argument Name Description Required
username The username (samAccountName) of the user to modify. Required
base-dn Root (for example, DC=domain,DC=com). Optional

Context Output

There is no context output for this command.

ad-modify-password-never-expire


Modifies the AD account attribute “Password Never Expire”.

Base Command

ad-modify-password-never-expire

Required Permissions

Requires Read userAccountControl and write userAccountControl permissions.

Input

Argument Name Description Required
username The sAMAccountName of the user to modify. Required
value Value to set “Password Never Expire”. Possible values are: true, false. Required

Context Output

There is no context output for this command.

Command Example
!ad-modify-password-never-expire username=jack value=true
Human Readable Output
AD account jack has set "password never expire" attribute. Value is set to True

ad-create-user


Creates an Active Directory user. This command requires a secure connection (SSL,TLS).

Base Command

ad-create-user

Required Permissions

Requires Create, delete, and manage user accounts permissions.

Input

Argument Name Description Required
username The username (samAccountName) of the user to modify. Required
password The initial password to set for the user. The user is requested to change the password after login. Required
user-dn The user DN. Required
display-name The user display name. Optional
description A short description of the user. Optional
email The user email. Optional
telephone-number The user telephone number. Optional
title The user job title. Optional
custom-attributes Sets basic or custom attributes of the user object. For example, custom-attributes=”{"notes":"a note about the contact","company":"company name"}”. Optional

Context Output

There is no context output for this command.

Command Example
ad-create-user username="jack" password="1q2w3e4r!" user-dn="cn=jack,dc=demisto,dc=int" display-name="Samurai Jack"
Human Readable Output
Created user with DN: cn=jack,dc=demisto,dc=int

ad-search


Runs Active Directory queries.

For more information on the query syntax see the Microsoft documentation.

For more information on LDAP filters, see the LDAP documentation.

Base Command

ad-search

Required Permissions

Requires Read and Read and read all properties permissions in General permissions.

Input

Argument Name Description Required
filter Enables you to define search criteria in the Query Active Directory using Active Directory syntax. For example, the following query searches for all user objects except Andy: “(&(objectCategory=person)(objectClass=user)(!(cn=andy)))”. Note: If you have special characters such as “”,”(“,or “" the character must be preceded by two backslashes “\”. For example, to use “”, type “\*”. For more information about search filters, see syntax: https://docs.microsoft.com/en-us/windows/win32/adsi/search-filter-syntax. Required
base-dn Root. For example, DC=domain,DC=com). By default, the Base DN configured for the instance is used. Optional
attributes A CSV list of the object attributes to return. For example, “dn,memberOf”. To return all object attributes, specify ‘ALL’. Optional
size-limit The maximum number of records to return. Default is 50. Optional
time-limit The maximum time to pull records (in seconds). Optional
context-output Whether to output the search results to the context. Possible values are: yes, no. Default is yes. Optional
page-size The page size to query. The size-limit value will be ignored. Optional
page-cookie An opaque string received in a paged search, used for requesting subsequent entries. Optional

Context Output

Path Type Description
ActiveDirectory.Search.dn string The distinguished names that match the query.
ActiveDirectory.Search unknown The result of the search.
ActiveDirectory.SearchPageCookie string An opaque string received in a paged search, used for requesting subsequent entries.
Command Example
ad-search filter="(&(objectCategory=person)(objectClass=user)(!(cn=andy)))"
Context Example
{
    "ActiveDirectory.Search": [
        {
            "dn": "CN=demistoadmin,CN=Users,DC=demisto,DC=int"
        },
        {
            "dn": "CN=Guest,CN=Users,DC=demisto,DC=int"
        }
    ]
}
Human Readable Output

Active Directory Search

dn
CN=demistoadmin,CN=Users,DC=demisto,DC=int
CN=Guest,CN=Users,DC=demisto,DC=int

ad-add-to-group


Adds an Active Directory user or computer to a group.

Base Command

ad-add-to-group

Input

Argument Name Description Required
username The username of the user to add to the group. If this argument is not specified, the computer name argument must be specified.\n Supports single or comma delimited list of usernames. Optional
computer-name The name of the computer to add to the group. If this argument is not specified, the username argument must be specified. Optional
group-cn The name of the group to add the user to. Required
base-dn Root. For example, DC=domain,DC=com. By default, the Base DN configured for the instance is used. Optional
nested_group_cn The name of the group to add as a member of the group specified group-cn. Optional

Context Output

There is no context output for this command.

ad-remove-from-group


Removes an Active Directory user or computer from a group.

Base Command

ad-remove-from-group

Required Permissions

Requires Create, delete, and manage groups permissions.

Input

Argument Name Description Required
username The name of the user to remove from the group. If this argument is not specified, the computer name argument must be specified. Optional
computer-name The name of the computer to remove from the group. If this argument is not specified, the username argument must be specified. Optional
group-cn The name of the group to remove the user from. Required
base-dn Root. For example, DC=domain,DC=com). By default, the Base DN configured for the instance is used. Optional

Context Output

There is no context output for this command.

Command Example
ad-remove-from-group username="jack" group-cn="Users"
Human Readable Output
Object with dn CN=jack,DC=demisto,DC=int removed from group Users

ad-update-user


Updates attributes of an existing Active Directory user.

Base Command

ad-update-user

Required Permissions

Requires Write All Properties and Read All Properties permission from User objects.

Input

Argument Name Description Required
username The username of the account to update (sAMAccountName). Required
attribute-name The name of the attribute to modify. For example, sn, displayName, mail, and so on. Required
attribute-value The value to change the attribute to. When attribute-type is “byte”, provide a comma-separated list of decimal integers (0-255), e.g. “0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0” for logonHours. Required
attribute-type The type of the attribute value. Set to “byte” for binary/Octet String attributes such as logonHours, objectSID, or objectGUID. When set, attribute-value must be a comma-separated list of decimal integers (0-255). Possible values are: byte. Optional
base-dn Root. For example, DC=domain,DC=com. By default, the Base DN configured for the instance is used. Optional

Context Output

There is no context output for this command.

Command Example
!ad-update-user attribute-name=description attribute-value=Samurai username=jack
Human Readable Output
Updated user's description to Samurai

ad-delete-user


Deletes an Active Directory user.

Base Command

ad-delete-user

Required Permissions

Requires Create, delete, and manage user accounts permissions.

Input

Argument Name Description Required
user-dn The DN of the user to delete. Required

Context Output

There is no context output for this command.

Command Example
!ad-delete-user user-dn="cn=jack,dc=demisto,dc=int"
Human Readable Output
Deleted object with dn cn=jack,dc=demisto,dc=int

ad-create-contact


Creates an Active Directory contact.

Base Command

ad-create-contact

Required Permissions

Requires full control permission from Contact objects.

Input

Argument Name Description Required
contact-dn The contact DN. Required
display-name The contact display name. Optional
description The short description of the contact. Optional
email The email address of the contact. Optional
telephone-number The contact telephone number. Optional
custom-attributes Sets basic or custom attributes of the contact object. For example, custom-attributes=”{"notes":"some note about the contact","company":"some company"}.”. Optional
title The contact job title. Optional

Context Output

There is no context output for this command.

Command Example
!ad-create-contact contact-dn="cn=jack,dc=demisto,dc=int" description="Samurai" email=jack@company.com
Human Readable Output
Created contact with DN: cn=jack,dc=demisto,dc=int

ad-update-contact


Updates attributes of an existing Active Directory contact.

Base Command

ad-update-contact

Required Permissions

Requires Write All Properties and Read All Properties permission from Contact objects.

Input

Argument Name Description Required
contact-dn The contact DN. Required
attribute-name The attribute name to update. Required
attribute-value The attribute value to update. When attribute-type is “byte”, provide a comma-separated list of decimal integers (0-255). Required
attribute-type The type of the attribute value. Set to “byte” for binary/Octet String attributes. When set, attribute-value must be a comma-separated list of decimal integers (0-255). Possible values are: byte. Optional

Context Output

There is no context output for this command.

Command Example
ad-update-contact contact-dn="cn=Jack,dc=demisto,dc=int" attribute-name="displayName" attribute-value="Jack H."
Human Readable Output
Updated contact’s displayName to: Jack H.

ad-disable-account


Disables an Active Directory user account.

Base Command

ad-disable-account

Required Permissions

Requires Read userAccountControl and write userAccountControl permissions.

Input

Argument Name Description Required
username The username of the account to disable (sAMAccountName). Required
base-dn Root (e.g., DC=domain,DC=com). By default, the Base DN configured for the instance is used. Optional

Context Output

There is no context output for this command.

Command Example
ad-disable-account username="jack"
Human Readable Output
User “CN=jack,DC=demisto,DC=int” has been disabledUser jack was disabled

ad-enable-account


Enables a previously disabled Active Directory account.

Base Command

ad-enable-account

Required Permissions

Requires Read userAccountControl and write userAccountControl permissions.

Input

Argument Name Description Required
username The username of the account to enable (sAMAccountName). Required
base-dn Root. For example, DC=domain,DC=com). By default, the Base DN configured for the instance is used. Optional
restore_user If true, the command will enable the user with his restored options. Possible values are: true, false. Optional

Context Output

There is no context output for this command.

Command Example
ad-enable-account username="jack"
Human Readable Output
User jack was enabledUser “CN=jack,DC=demisto,DC=int” has been enabled

ad-unlock-account


Unlocks a previously locked Active Directory user account.

Base Command

ad-unlock-account

Required Permissions

Requires Read lockoutTime and write lockoutTime permissions.

Input

Argument Name Description Required
username The username of the account to unlock (sAMAccountName). Required
base-dn Root. For example, DC=domain,DC=com. By default, the Base DN configured for the instance is used. Optional

Context Output

There is no context output for this command.

Command Example
!ad-unlock-account username=mooncake
Human Readable Output
User "CN=mooncake,CN=Users,DC=demisto,DC=int" has been unlocked

ad-set-new-password


Sets a new password for an Active Directory user. This command requires a secure connection (SSL,TLS).

Base Command

ad-set-new-password

Required Permissions

Requires Reset password permissions.

Input

Argument Name Description Required
username The username of the account to disable (sAMAccountName). Required
password The password to set for the user. Required
base-dn Root. For example, DC=domain,DC=com. Base DN configured for the instance is used as default. Optional

Context Output

There is no context output for this command.

Command Example
!ad-set-new-password username="NoaCo" password="noni1q2w3e!"
Human Readable Output
User password successfully set

ad-modify-computer-ou


Modifies the computer organizational unit within a domain.

Base Command

ad-modify-computer-ou

Required Permissions

Requires Write All Properties permission from Computer objects.

Input

Argument Name Description Required
computer-name The name of the computer to modify. Required
full-superior-dn Superior DN. For example, OU=computers,DC=domain,DC=com (the specified domain must be the same as the current computer domain). Optional

Context Output

There is no context output for this command.

Command Example
!ad-modify-computer-ou computer-name=mike full-superior-dn=OU=Sarah,DC=demisto,DC=int
Human Readable Output
"mike" was successfully moved to "OU=Sarah,DC=demisto,DC=int"

ad-modify-user-ou


Modifies the user organizational unit within a domain.

Base Command

ad-modify-user-ou

Required Permissions

Requires Write All Properties permission from Computer objects.

Input

Argument Name Description Required
user-name The name of the user to modify. Required
full-superior-dn Superior DN. For example, OU=users,DC=domain,DC=com (the specified domain must be the same as the current user domain). Optional

Context Output

There is no context output for this command.

Command Example
!ad-modify-user-ou user-name=username full-superior-dn=OU=users,DC=demisto,DC=int
Human Readable Output
"username" was successfully moved to "OU=users,DC=demisto,DC=int"

ad-get-user


Retrieves detailed information about a user account. The user can be specified by name, email address, or as an Active Directory Distinguished Name (DN). If no filter is specified, all users are returned.

Base Command

ad-get-user

Required Permissions

Requires Read all user information permissions.

Input

Argument Name Description Required
dn The Distinguished Name of the user in which to return information. Optional
name The name of the user to return information. Optional
attributes Adds AD attributes of the resulting objects to the default attributes. Optional
attributes-to-exclude Removes AD attributes of the resulting objects from the attributes. Optional
custom-field-type Queries users by custom field type. Optional
custom-field-data Queries users by custom field data (relevant only if the custom-field-type argument is provided). Optional
username Queries users by the samAccountName attribute. Optional
sAMAccountName Queries users by the samAccountName attribute. Optional
limit The maximum number of objects to return. Default is 20. Optional
email Queries by the user’s email address. Optional
user-account-control-out Whether to include verbose translation for UserAccountControl flags. Default is false. Possible values are: true, false. Default is false. Optional
page-size The page size to query. The limit value will be ignored. Optional
page-cookie An opaque string received in a paged search, used for requesting subsequent entries. Optional

Context Output

Path Type Description
ActiveDirectory.Users.dn string The user distinguished name.
ActiveDirectory.Users.displayName string The user display name.
ActiveDirectory.Users.name string The user common name.
ActiveDirectory.Users.sAMAccountName string The user sAMAccountName.
ActiveDirectory.Users.userAccountControl number The user account control flag.
ActiveDirectory.Users.mail string The user email address.
ActiveDirectory.Users.manager string The manager of the user.
ActiveDirectory.Users.memberOf string Groups in which the user is a member.
ActiveDirectory.Users.userAccountControlFields.SCRIPT bool Whether the login script is run. Works for *Windows Server 2012 R2*.
ActiveDirectory.Users.userAccountControlFields.ACCOUNTDISABLE bool Whether the user account is disabled. Works for *Windows Server 2012 R2*.
ActiveDirectory.Users.userAccountControlFields.HOMEDIR_REQUIRED bool Whether the home folder is required. Works for *Windows Server 2012 R2*.
ActiveDirectory.Users.userAccountControlFields.LOCKOUT bool Whether the user is locked out. Works for *Windows Server 2012 R2*.
ActiveDirectory.Users.userAccountControlFields.PASSWD_NOTREQD bool Whether the password is required. Works for *Windows Server 2012 R2*.
ActiveDirectory.Users.userAccountControlFields.PASSWD_CANT_CHANGE bool Whether the user can change the password. Works for *Windows Server 2012 R2*.
ActiveDirectory.Users.userAccountControlFields.ENCRYPTED_TEXT_PWD_ALLOWED bool Whether the user can send an encrypted password. Works for *Windows Server 2012 R2*.
ActiveDirectory.Users.userAccountControlFields.TEMP_DUPLICATE_ACCOUNT bool Whether this is an account for users whose primary account is in another domain. Works for *Windows Server 2012 R2*.
ActiveDirectory.Users.userAccountControlFields.NORMAL_ACCOUNT bool Whether this is a default account type that represents a typical user. Works for *Windows Server 2012 R2*.
ActiveDirectory.Users.userAccountControlFields.INTERDOMAIN_TRUST_ACCOUNT bool Whether the account is permitted to trust a system domain that trusts other domains. Works for *Windows Server 2012 R2*.
ActiveDirectory.Users.userAccountControlFields.WORKSTATION_TRUST_ACCOUNT bool Whether this is a computer account for a computer running Microsoft Windows NT 4.0 Workstation, Microsoft Windows NT 4.0 Server, Microsoft Windows 2000 Professional, or Windows 2000 Server and is a member of this domain.
ActiveDirectory.Users.userAccountControlFields.SERVER_TRUST_ACCOUNT bool Whether this is a computer account for a domain controller that is a member of this domain. Works for *Windows Server 2012 R2*.
ActiveDirectory.Users.userAccountControlFields.DONT_EXPIRE_PASSWORD bool Whether to never expire the password on the account.
ActiveDirectory.Users.userAccountControlFields.MNS_LOGON_ACCOUNT bool Whether this is an MNS login account.
ActiveDirectory.Users.userAccountControlFields.SMARTCARD_REQUIRED bool Whether to force the user to log in by using a smart card.
ActiveDirectory.Users.userAccountControlFields.TRUSTED_FOR_DELEGATION bool Whether the service account (the user or computer account) under which a service runs is trusted for Kerberos delegation.
ActiveDirectory.Users.userAccountControlFields.NOT_DELEGATED bool Whether the security context of the user isn’t delegated to a service even if the service account is set as trusted for Kerberos delegation.
ActiveDirectory.Users.userAccountControlFields.USE_DES_KEY_ONLY bool Whether to restrict this principal to use only Data Encryption Standard (DES) encryption types for keys.
ActiveDirectory.Users.userAccountControlFields.DONT_REQ_PREAUTH bool Whether this account require Kerberos pre-authentication for logging on.
ActiveDirectory.Users.userAccountControlFields.PASSWORD_EXPIRED bool Whether the user password expired.
ActiveDirectory.Users.userAccountControlFields.TRUSTED_TO_AUTH_FOR_DELEGATION bool Whether the account is enabled for delegation.
ActiveDirectory.Users.userAccountControlFields.PARTIAL_SECRETS_ACCOUNT bool Whether the account is a read-only domain controller (RODC).
ActiveDirectory.UsersPageCookie string An opaque string received in a paged search, used for requesting subsequent entries.
Account.DisplayName string The user display name.
Account.Groups string Groups for which the user is a member.
Account.Manager string The user manager.
Account.ID string The user distinguished name.
Account.Username string The user samAccountName.
Account.Email string The user email address.
Command Example
!ad-get-user name=*
Human Readable Output

Active Directory - Get Users

dn displayName mail manager memberOf name sAMAccountName userAccountControl
CN=demistoadmin,CN=Users,DC=demisto,DC=int demistoadmin demistoadmin@demisto.int   CN=Discovery Management,OU=Microsoft Exchange Security Groups,DC=demisto,DC=int,CN=Organization Management,OU=Microsoft Exchange Security Groups,DC=demisto,DC=int,CN=Group Policy Creator Owners,CN=Users,DC=demisto,DC=int,CN=Domain Admins,CN=Users,DC=demisto,DC=int,CN=Enterprise Admins,CN=Users,DC=demisto,DC=int,CN=Schema Admins,CN=Users,DC=demisto,DC=int,CN=Administrators,CN=Builtin,DC=demisto,DC=int demistoadmin demistoadmin 66048
CN=Guest,CN=Users,DC=demisto,DC=int       CN=Guests,CN=Builtin,DC=demisto,DC=int Guest Guest 66082

ad-get-computer


Retrieves detailed information about a computer account. The computer can be specified by name, email address, or as an Active Directory Distinguished Name (DN). If no filters are provided, all computers are returned.

Base Command

ad-get-computer

Required Permissions

Requires Read and Read and read all properties permissions from Computer objects.

Input

Argument Name Description Required
dn The computer’s DN. Optional
name The name of the computer to return information about. Optional
attributes Adds AD attributes of the resulting objects to the default attributes. Optional
custom-field-data Search computers by custom field data (relevant only if the customFieldType argument is provided). Optional
custom-field-type Search the computer by custom field type. Optional
limit The maximum number of records to return. Optional
page-size The page size to query. The value limit will be ignored. Optional
page-cookie An opaque string received in a paged search, used for requesting subsequent entries. Optional

Context Output

Path Type Description
ActiveDirectory.Computers.dn unknown The computer distinguished name.
ActiveDirectory.Computers.memberOf unknown Groups for which the computer is listed.
ActiveDirectory.Computers.name unknown The computer name.
Endpoint.ID unknown The computer DN.
Endpoint.Hostname unknown The computer host name.
Endpoint.Groups unknown Groups for which the computer is listed as a member.
ActiveDirectory.ComputersPageCookie string An opaque string received in a paged search, used for requesting subsequent entries.
Command Example
ad-get-computer name=noapc
Context Example
{
    "ActiveDirectory.Computers":
         [ { "dn": "CN=noapc,OU=Shani,DC=demisto,DC=int",
             "memberOf": [ "CN=Exchange Servers,OU=Microsoft Exchange Security Groups,DC=demisto,DC=int" ],
             "name": [ "noapc" ] } ],
             "Endpoint": [ { "Hostname": [ "noapc" ],
             "Type": "AD", "ID": "CN=noapc,OU=Shani,DC=demisto,DC=int",
             "Groups": [ "CN=Exchange Servers,OU=Microsoft Exchange Security Groups,DC=demisto,DC=int" ]
        } ]
}
Human Readable Output

Active Directory - Get Computers

dn memberOf name
CN=noapc,OU=Shani,DC=demisto,DC=int CN=Exchange Servers,OU=Microsoft Exchange Security Groups,DC=demisto,DC=int noapc

ad-get-group-members


Retrieves the list of users or computers that are members of the specified group.

Base Command

ad-get-group-members

Required Permissions

Requires Read members permissions.

Input

Argument Name Description Required
group-dn The Distinguished Name of the Group’s Active Directory. Required
member-type The type of members to search. Can be: “Person”, or “computer”. Default is person. Possible values are: person, computer, group. Default is person. Required
attributes CSV list of attributes to include in the results, in addition to the default attributes. Optional
time_limit Time limit (in seconds) for the search to run. Default is 180. Optional
disable-nested-search Whether to disable recursive retrieval of group memberships of a user. Possible values are: false, true. Default is false. Optional
sAMAccountName Queries results by the samAccountName attribute. Default is *. Optional
limit The maximum number of records to return. Optional
page-size The page size to query. The limit value will be ignored. Optional
page-cookie An opaque string received in a paged search, used for requesting subsequent entries. Optional

Context Output

Path Type Description
ActiveDirectory.Groups.dn string The group DN.
ActiveDirectory.Groups.members.dn string The group member DN.
ActiveDirectory.Groups.members.category string The group members category.
ActiveDirectory.GroupsPageCookie string An opaque string received in a paged search, used for requesting subsequent entries.

Command Example

!ad-get-group-members group-dn="CN=Group124,OU=DemistoMng,DC=demisto,DC=int"
Context Example
{ "Account":
         [ { "DisplayName": [ "User 671 User 671" ],
             "Email": null,
             "Groups": [ "CN=Group124,OU=DemistoMng,DC=demisto,DC=int", "CN=Group2,OU=DemistoMng,DC=demisto,DC=int" ],
             "ID": "CN=User 671 User 671,OU=DemistoMng,DC=demisto,DC=int",
             "Managr": [],
             "Type": "AD",
             "Username": null } ],
  "ActiveDirectory":
                  { "Groups": { "dn": "CN=Group124,OU=DemistoMng,DC=demisto,DC=int",
                    "members": [ { "category": "person", "dn": "CN=User 671 User 671,OU=DemistoMng,DC=demisto,DC=int" } ] },
                    "Users": { "displayName": [ "User 671 User 671" ],
                    "dn": "CN=User 671 User 671,OU=DemistoMng,DC=demisto,DC=int",
                    "mail": [ "test@demisto.int" ],
                    "manager": [],
                    "memberOf": [ "CN=Group124,OU=DemistoMng,DC=demisto,DC=int",
                    "CN=Group2,OU=DemistoMng,DC=demisto,DC=int" ],
                    "name": [ "User 671 User 671" ],
                    "sAMAccountName": [ "User 671User 671" ],
                    "userAccountControl": [ 514 ] }
                  }
}
Human Readable Output

###Active Directory - Get Group Members

dn displayName mail manager memberOf name sAMAccountName userAccountControl
CN=User 671 User User 671 test@demisto.int   CN=Group124,OU=DemistoMng,DC=demisto,DC=int User 671 User 671User 671 514
671,OU=DemistoMng,DC=demisto,DC=int User 671     CN=Group2,OU=DemistoMng,DC=demisto,DC=int User 671 User 671User 671 514

ad-create-group


Creates a new security or distribution Active Directory group.

Base Command

ad-create-group

Required Permissions

Requires Create, delete, and manage groups permissions.

Input

Argument Name Description Required
name The Active Directory name of the group. Required
group-type The type of group. Can be: “security”, or “distribution”. Possible values are: security, distribution. Required
dn The Full Distinguished Name (DN) of the group. Use double quotes (“”) rather than single quotes (‘’) when initializing this command. Required
members The Full DN Of users or groups that will be members of the newly created group. Optional

Context Output

There is no context output for this command.

ad-delete-group


Deletes an existing Active Directory security or distribution group.

Base Command

ad-delete-group

Required Permissions

Requires Create, delete, and manage groups permissions.

Input

Argument Name Description Required
dn The Active Directory Distinguished Name (DN) of the group. Required

Context Output

There is no context output for this command.

ad-update-group


Updates attributes of an existing Active Directory group.

Base Command

ad-update-group

Required Permissions

Requires Create, delete, and manage groups permissions.

Input

Argument Name Description Required
groupname The group name of the group to update (sAMAccountName). Optional
attributename The name of the attribute to modify. For example, Description and displayName. Required
attributevalue The value of the attribute to change. When attribute-type is “byte”, provide a comma-separated list of decimal integers (0-255). Required
attribute-type The type of the attribute value. Set to “byte” for binary/Octet String attributes. When set, attributevalue must be a comma-separated list of decimal integers (0-255). Possible values are: byte. Optional
basedn Root. For example, DC=domain,DC=com. By default, the Base DN configured for the instance is used. Optional

Context Output

There is no context output for this command.

ad-test-credentials


Test given credentials.

Base Command

ad-test-credentials

Input

Argument Name Description Required
username Username to test. By itself or formatted as SERVER_IP\USERNAME Required
password Password to test. Required

Context Output

Path Type Description
ActiveDirectory.ValidCredentials Unknown List of usernames that successfully logged in.

iam-create-user


Creates an Active Directory user. This command requires a secure connection (SSL,TLS).
Used in the IAM premium pack.

Base Command

iam-create-user

Required Permissions

Requires Create, delete, and manage user accounts permissions.

Input

Argument Name Description Required
user-profile A User Profile indicator that contains user information, such as name, email address, etc. Required
allow-enable When set to true, after the command execution the status of the user in the 3rd-party integration will be active. Possible values are: true, false. Default is true. Optional

Context Output

Path Type Description
IAM.UserProfile Unknown The user profile.
IAM.Vendor.active Boolean If true, the employee status is active.
IAM.Vendor.brand String The integration name.
IAM.Vendor.details Unknown Tells the user if the API was successful, otherwise provides error information.
IAM.Vendor.email String The employee email address.
IAM.Vendor.errorCode Number The HTTP error response code.
IAM.Vendor.errorMessage String The reason the API failed.
IAM.Vendor.id String The employee user ID in the app.
IAM.Vendor.instanceName Unknown The name of the integration instance.
IAM.Vendor.success Boolean If true, the command executed successfully.
IAM.Vendor.username String The employee username in the app.
IAM.Vendor.action String The command name.

Command Example

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

Human Readable Output

Create User Results

brand instanceName success active id username email details
Active Directory Query IAM_instance_1 true true   testdemisto2 testdemisto2@paloaltonetworks.com 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”: “Active Directory Query”, “name”: “Active Directory Query”}}}

iam-get-user


Retrieves a single user resource.
Used in the IAM premium pack.

Base Command

iam-get-user

Required Permissions

Requires Read all user information permissions.

Input

Argument Name Description Required
user-profile A User Profile indicator that contains user information, such as name and email address. Required

Context Output

Path Type Description
IAM.UserProfile Unknown The user profile.
IAM.Vendor.active Boolean If true the employee status is active.
IAM.Vendor.brand String The integration name.
IAM.Vendor.details Unknown Tells the user if the API was successful, otherwise provides error information.
IAM.Vendor.email String The employee email address.
IAM.Vendor.errorCode Number The HTTP error response code.
IAM.Vendor.errorMessage String The reason the API failed.
IAM.Vendor.id String The employee user ID in the app.
IAM.Vendor.instanceName Unknown The integration instance name.
IAM.Vendor.success Boolean If true, the command was executed successfully.
IAM.Vendor.username String The employee username in the app.
IAM.Vendor.action String The command name.

Command Example

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

Human Readable Output

Get User Results

brand instanceName success active id username email details
Active Directory Query IAM_instance_1 true true   testdemisto2 testdemisto2@paloaltonetworks.com 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”: “Active Directory Query”, “name”: “Active Directory Query”}}}

iam-update-user


Updates an existing user with the data in the User Profile indicator that is passed in the user-profile argument.
Used in the IAM premium pack.

Base Command

iam-update-user

Required Permissions

Requires Create, delete, and manage user accounts permissions.

Input

Argument Name Description Required
user-profile A User Profile indicator that contains user information, such as name and email address. Required
allow-enable When set to true, after the command executes the user status in the 3rd-party integration is active. Possible values are: true, false. Default is true. Optional

Context Output

Path Type Description
IAM.UserProfile Unknown The user profile
IAM.Vendor.active Boolean Gives the active status of user. Can be true or false.
IAM.Vendor.brand String The integration name.
IAM.Vendor.details Unknown Tells the user if the API was successful, otherwise provides error information.
IAM.Vendor.email String The employee email address.
IAM.Vendor.errorCode Number The HTTP error response code.
IAM.Vendor.errorMessage String The reason the API failed.
IAM.Vendor.id String The employee user ID in the app.
IAM.Vendor.instanceName Unknown The integration instance name.
IAM.Vendor.success Boolean If true, the command executed successfully.
IAM.Vendor.username String The employee username in the app.
IAM.Vendor.action String The command name.

Command Example

!iam-update-user user-profile={\"email\":\"testdemisto22@paloaltonetworks.com\", \"name\":\"testdemisto2\"}

Human Readable Output

Update User Results

brand instanceName success active id username email details
Active Directory Query IAM_instance_1 true true   testdemisto2 testdemisto22@paloaltonetworks.com 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”: “Active Directory Query”, “name”: “Active Directory Query”}}}

iam-disable-user


Disables a user.
Used in the IAM premium pack.

Base Command

iam-disable-user

Required Permissions

Requires Read userAccountControl and write userAccountControl permissions.

Input

iam-disable-user


Disables a user.

Base Command

iam-disable-user

Input

Argument Name Description Required
user-profile A User Profile indicator that contains user information, such as name and email address. Required

Context Output

Path Type Description
IAM.UserProfile Unknown The user profile.
IAM.Vendor.active Boolean Gives the active status of user. Can be true or false.
IAM.Vendor.brand String The integration name.
IAM.Vendor.details Unknown Tells the user if the API was successful, otherwise provides error information.
IAM.Vendor.email String The employee email address.
IAM.Vendor.errorCode Number The HTTP error response code.
IAM.Vendor.errorMessage String The reason the API failed.
IAM.Vendor.id String The employee user ID in the app.
IAM.Vendor.instanceName Unknown The integration instance name.
IAM.Vendor.success Boolean If true, the command was executed successfully.
IAM.Vendor.username String The employee username in the app.
IAM.Vendor.action String The command name.

There are no input arguments for this command.

Context Output

There is no context output for this command.

Incident Mirroring

You can enable incident mirroring between Cortex XSOAR incidents and Active Directory Query v2 corresponding events (available from Cortex XSOAR version 6.0.0).

To set up the mirroring, enable Fetching incidents in your instance configuration.

Newly fetched incidents will be mirrored in the chosen direction. However, this selection does not affect existing incidents.

Important Note: To ensure the mirroring works as expected, mappers are required, both for incoming and outgoing, to map the expected fields in Cortex XSOAR and Active Directory Query v2.

Breaking changes from the previous version of this integration - Active Directory Query v2

The following sections list the changes in this version.

Arguments

The following arguments were added in this version

In the ad-get-user command:

  • attributes-to-exclude

get-mapping-fields


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

Base Command

get-mapping-fields

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

There is no context output for this command.

Configuration parameters

  • server_ip — Server IP address (for example, 192.168.0.1) (required)
  • port — Server port. If not specified, the default port is 389 for LDAP, 636 for LDAPS, or 3268 for global catalog servers.
  • credentials — Username (required)
  • ntlm — NTLM authentication
  • base_dn — Base DN (for example "dc=company,dc=com") (required)
  • page_size — Page size
  • secure_connection — Secure Connection (required)
  • ssl_version — SSL Version
  • unsecure — Trust any certificate (not secure)
  • verify_base_dn — Verify base DN on every command
  • mapper-in — Incoming Mapper (required)
  • mapper-out — Outgoing Mapper (required)
  • group-cn — Group CN for terminated employees
  • create-if-not-exists — Create user if does not exist
  • default_base_query — Default base query to use in the ad-get-user command

Commands (28)

  • ad-add-to-group

    Adds an Active Directory user or computer to a group.

  • ad-create-contact

    Creates an Active Directory contact.

  • ad-create-group

    Creates a new security or distribution Active Directory group.

  • ad-create-user

    Creates an Active Directory user. This command requires a secure connection (SSL,TLS).

  • ad-delete-group

    Deletes an existing Active Directory security or distribution group.

  • ad-delete-user

    Deletes an Active Directory user.

  • ad-disable-account

    Disables an Active Directory user account.

  • ad-enable-account

    Enables a previously disabled Active Directory account.

  • ad-expire-password

    Expires the password of an Active Directory user.

  • ad-get-computer

    Retrieves detailed information about a computer account. The computer can be specified by name, email address, or as an Active Directory Distinguished Name (DN). If no filters are provided, all computers are returned.

  • ad-get-group-members

    Retrieves the list of users or computers that are members of the specified group.

  • ad-get-user

    Retrieves detailed information about a user account. The user can be specified by name, email address, or as an Active Directory Distinguished Name (DN). If no filter is specified, all users are returned.

  • ad-modify-computer-ou

    Modifies the computer organizational unit within a domain.

  • ad-modify-password-never-expire

    Modifies the AD account attribute "Password Never Expire".

  • ad-modify-user-ou

    Modifies the user organizational unit within a domain.

  • ad-remove-from-group

    Removes an Active Directory user or computer from a group.

  • ad-search

    Runs Active Directory queries.

  • ad-set-new-password

    Sets a new password for an Active Directory user. This command requires a secure connection (SSL,TLS).

  • ad-test-credentials

    Test given credentials.

  • ad-unlock-account

    Unlocks a previously locked Active Directory user account.

  • ad-update-contact

    Updates attributes of an existing Active Directory contact.

  • ad-update-group

    Updates attributes of an existing Active Directory group.

  • ad-update-user

    Updates attributes of an existing Active Directory user.

  • get-mapping-fields

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

  • iam-create-user

    Creates an Active Directory user. This command requires a secure connection (SSL,TLS).

  • iam-disable-user

    Disables a user.

  • iam-get-user

    Retrieves a single user resource.

  • iam-update-user

    Updates an existing user with the data in the User Profile indicator that is passed in the user-profile argument.

import os
import ssl
from datetime import datetime

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401
from ldap3 import (
    ALL_ATTRIBUTES,
    AUTO_BIND_NO_TLS,
    AUTO_BIND_TLS_BEFORE_BIND,
    BASE,
    NTLM,
    SUBTREE,
    Connection,
    Entry,
    ObjectDef,
    Reader,
    Server,
    Tls,
)
from ldap3.core.exceptions import LDAPBindError, LDAPSocketOpenError, LDAPSocketReceiveError, LDAPStartTLSError
from ldap3.extend import microsoft
from ldap3.utils.conv import escape_filter_chars
from ldap3.utils.log import (
    EXTENDED,
    get_library_log_detail_level,
    set_library_log_detail_level,
    set_library_log_hide_sensitive_data,
)

""" GLOBAL VARS """

CIPHERS_STRING = (
    "@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
    "DH+AESGCM:ECDH+AES:DH+AES:RSA+ANESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
)  # Allowed ciphers for SSL/TLS
DEFAULT_TIMEOUT = 120  # timeout for ssl/tls socket
START_TLS = "Start TLS"
TLS = "TLS"
SSL = "SSL"
SSL_VERSIONS = {
    "None": None,
    "TLS": ssl.PROTOCOL_TLS,
    "TLSv1": ssl.PROTOCOL_TLSv1,  # guardrails-disable-line
    "TLSv1_1": ssl.PROTOCOL_TLSv1_1,  # guardrails-disable-line
    "TLSv1_2": ssl.PROTOCOL_TLSv1_2,
    "TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
}
# global connection
connection: Connection | None = None

# userAccountControl is a bitmask used to store a number of settings.
# find more at:
# https://support.microsoft.com/en-gb/help/305144/how-to-use-the-useraccountcontrol-flags-to-manipulate-user-account-pro
DEFAULT_OUTGOING_MAPPER = "User Profile - Active Directory (Outgoing)"
DEFAULT_INCOMING_MAPPER = "User Profile - Active Directory (Incoming)"

COMMON_ACCOUNT_CONTROL_FLAGS = {
    512: "Enabled Account",
    514: "Disabled account",
    544: "Password Not Required",
    4096: "Workstation/server",
    66048: "Enabled, password never expires",
    66050: "Disabled, password never expires",
    66080: "Enables, password never expires, password not required.",
    532480: "Domain controller",
}
NORMAL_ACCOUNT = 512
DISABLED_ACCOUNT = 514
PASSWORD_NOT_REQUIRED = 544
INACTIVE_LIST_OPTIONS = [514, 546, 66050, 66082, 262658, 262690, 328226]
DEFAULT_LIMIT = 20

# common attributes for specific AD objects
DEFAULT_PERSON_ATTRIBUTES = ["name", "displayName", "memberOf", "mail", "sAMAccountName", "manager", "userAccountControl"]
DEFAULT_COMPUTER_ATTRIBUTES = ["name", "memberOf"]
DEFAULT_GROUP_ATTRIBUTES = ["name", "memberOf"]
FIELDS_THAT_CANT_BE_MODIFIED = ["dn", "cn", "ou"]

""" HELPER FUNCTIONS """


def get_ssl_version(ssl_version):
    """
    Returns the ssl version object according to the user's selection.
    """
    version = SSL_VERSIONS.get(ssl_version)
    if version:
        demisto.info(f"SSL/TLS protocol version is {ssl_version} ({version}).")
    else:  # version is None
        demisto.info("SSL/TLS protocol version is None (the default value of the ldap3 Tls object).")

    return version


def get_tls_object(unsecure, ssl_version):
    """
    Returns a TLS object according to the user's selection of the 'Trust any certificate' checkbox.
    """
    if unsecure:  # Trust any certificate is checked
        # Trust any certificate = True means that we do not require validation of the LDAP server's certificate,
        # and allow the use of all possible ciphers.
        tls = Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS_STRING, version=get_ssl_version(ssl_version))

    else:  # Trust any certificate is unchecked
        # Trust any certificate = False means that the LDAP server's certificate must be valid -
        # i.e if the server's certificate is not valid the connection will fail.
        tls = Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=get_ssl_version(ssl_version))

    return tls


def initialize_server(host, port, secure_connection, unsecure, ssl_version):
    """
    Uses the instance configuration to initialize the LDAP server.
    Supports both encrypted and non encrypted connection.

    :param host: host or ip
    :type host: string
    :param port: port or None
    :type port: number
    :param secure_connection: SSL, TLS, Start TLS or None
    :type secure_connection: string
    :param unsecure: trust any certificate
    :type unsecure: boolean
    :param ssl_version: ssl version
    :type unsecure: string
    :return: ldap3 Server
    :rtype: Server
    """
    if secure_connection == TLS:
        # Kept the TLS option for backwards compatibility only.
        # For establishing a secure connection via SSL/TLS protocol - use the 'SSL' option.
        # For establishing a secure connection via Start TLS - use the 'Start TLS' option.
        demisto.debug(f"initializing sever with TLS (unsecure: {unsecure}). port: {port or 'default(636)'}")
        tls = get_tls_object(unsecure, ssl_version)
        if port:
            return Server(host, port=port, use_ssl=True, tls=tls)
        return Server(host, use_ssl=True, tls=tls)

    if secure_connection == SSL:  # Secure connection (SSL\TLS)
        demisto.info(f"Initializing LDAP sever with SSL/TLS (unsecure: {unsecure}). port: {port or 'default(636)'}")
        tls = get_tls_object(unsecure, ssl_version)
        return Server(host=host, port=port, use_ssl=True, tls=tls, connect_timeout=DEFAULT_TIMEOUT)

    elif secure_connection == START_TLS:  # Secure connection (STARTTLS)
        demisto.info(
            f"Initializing LDAP sever without a secure connection - Start TLS operation will be executed"
            f" during bind. (unsecure: {unsecure}). port: {port or 'default(389)'}"
        )
        tls = get_tls_object(unsecure, ssl_version)
        return Server(host=host, port=port, use_ssl=False, tls=tls, connect_timeout=DEFAULT_TIMEOUT)

    else:  # Unsecure (non encrypted connection initialized) - connection type is None
        demisto.info(f"Initializing LDAP sever without a secure connection. port: {port or 'default(389)'}")
        return Server(host=host, port=port, connect_timeout=DEFAULT_TIMEOUT)


def user_account_to_boolean_fields(user_account_control):
    """
    parse the userAccountControl into boolean values.
    following the values from:
    https://docs.microsoft.com/en-US/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties
    """
    return {
        "SCRIPT": bool(user_account_control & 0x0001),
        "ACCOUNTDISABLE": bool(user_account_control & 0x0002),
        "HOMEDIR_REQUIRED": bool(user_account_control & 0x0008),
        "LOCKOUT": bool(user_account_control & 0x0010),
        "PASSWD_NOTREQD": bool(user_account_control & 0x0020),
        "PASSWD_CANT_CHANGE": bool(user_account_control & 0x0040),
        "ENCRYPTED_TEXT_PWD_ALLOWED": bool(user_account_control & 0x0080),
        "TEMP_DUPLICATE_ACCOUNT": bool(user_account_control & 0x0100),
        "NORMAL_ACCOUNT": bool(user_account_control & 0x0200),
        "INTERDOMAIN_TRUST_ACCOUNT": bool(user_account_control & 0x0800),
        "WORKSTATION_TRUST_ACCOUNT": bool(user_account_control & 0x1000),
        "SERVER_TRUST_ACCOUNT": bool(user_account_control & 0x2000),
        "DONT_EXPIRE_PASSWORD": bool(user_account_control & 0x10000),
        "MNS_LOGON_ACCOUNT": bool(user_account_control & 0x20000),
        "SMARTCARD_REQUIRED": bool(user_account_control & 0x40000),
        "TRUSTED_FOR_DELEGATION": bool(user_account_control & 0x80000),
        "NOT_DELEGATED": bool(user_account_control & 0x100000),
        "USE_DES_KEY_ONLY": bool(user_account_control & 0x200000),
        "DONT_REQ_PREAUTH": bool(user_account_control & 0x400000),
        "PASSWORD_EXPIRED": bool(user_account_control & 0x800000),
        "TRUSTED_TO_AUTH_FOR_DELEGATION": bool(user_account_control & 0x1000000),
        "PARTIAL_SECRETS_ACCOUNT": bool(user_account_control & 0x04000000),
    }


def user_account_to_boolean_fields_msDS_user_account_control_computed(user_account_control):
    """
    parse the msDS-User-Account-Control-Computed into boolean values.
    following the values from:
    https://learn.microsoft.com/en-us/windows/win32/adschema/a-msds-user-account-control-computed
    """
    return {
        "PASSWORD_EXPIRED": bool(user_account_control & 0x800000),
        "LOCKOUT": bool(user_account_control & 0x0010),
    }


def account_entry(person_object, custom_attributes):
    # create an account entry from a person objects
    account = {
        "Type": "AD",
        "ID": person_object.get("dn"),
        "Email": person_object.get("mail"),
        "Username": person_object.get("sAMAccountName"),
        "DisplayName": person_object.get("displayName"),
        "Managr": person_object.get("manager"),
        "Manager": person_object.get("manager"),
        "Groups": person_object.get("memberOf"),
    }

    lower_cased_person_object_keys = {person_object_key.lower(): person_object_key for person_object_key in person_object}

    for attr in custom_attributes:
        try:
            account[attr] = person_object[attr]
        except KeyError as e:
            lower_cased_custom_attr = attr.lower()
            if lower_cased_custom_attr in lower_cased_person_object_keys:
                cased_custom_attr = lower_cased_person_object_keys.get(lower_cased_custom_attr, "")
                account[cased_custom_attr] = person_object[cased_custom_attr]
            else:
                demisto.error(f"Failed parsing custom attribute {attr}, error: {e}")

    return account


def endpoint_entry(computer_object, custom_attributes):
    # create an endpoint entry from a computer object
    endpoint = {
        "Type": "AD",
        "ID": computer_object.get("dn"),
        "Hostname": computer_object.get("name"),
        "Groups": computer_object.get("memberOf"),
    }

    lower_cased_person_object_keys = {person_object_key.lower(): person_object_key for person_object_key in computer_object}

    for attr in custom_attributes:
        if attr == "*":
            continue
        try:
            endpoint[attr] = computer_object[attr]
        except KeyError as e:
            lower_cased_custom_attr = attr.lower()
            if lower_cased_custom_attr in lower_cased_person_object_keys:
                cased_custom_attr = lower_cased_person_object_keys.get(lower_cased_custom_attr, "")
                endpoint[cased_custom_attr] = computer_object[cased_custom_attr]
            else:
                demisto.error(f"Failed parsing custom attribute {attr}, error: {e}")

    return endpoint


def group_entry(group_object, custom_attributes):
    # create an group entry from a group object
    group = {
        "Type": "AD",
        "ID": group_object.get("dn"),
        "Name": group_object.get("name"),
        "Groups": group_object.get("memberOf"),
    }

    lower_cased_person_object_keys = {person_object_key.lower(): person_object_key for person_object_key in group_object}

    for attr in custom_attributes:
        if attr == "*":
            continue
        try:
            group[attr] = group_object[attr]
        except KeyError as e:
            lower_cased_custom_attr = attr.lower()
            if lower_cased_custom_attr in lower_cased_person_object_keys:
                cased_custom_attr = lower_cased_person_object_keys.get(lower_cased_custom_attr, "")
                group[cased_custom_attr] = group_object[cased_custom_attr]
            else:
                demisto.error(f"Failed parsing custom attribute {attr}, error: {e}")

    return group


def base_dn_verified(base_dn: str) -> bool:
    """
    Verifies the base DN is configured correctly.
    Uses BASE scope, size limit of 1, and 'no attributes' OID for maximum performance.

    This function performs an optimized LDAP search that only checks if the base DN entry itself exists,
    rather than searching the entire directory tree. This reduces the complexity from O(n) to O(1).

    Args:
        base_dn: The base DN to verify (e.g., 'dc=example,dc=com')

    Returns:
        bool: True if the base DN is valid and accessible, False otherwise
    """
    assert connection is not None
    try:
        # Optimized search with three performance improvements:
        # 1. search_scope=BASE: Only looks at the DN itself (O(1) complexity)
        # 2. size_limit=1: Safety guard to ensure only one record is processed
        # 3. attributes=['1.1']: Special OID meaning 'return no attributes' (minimal data transfer)
        success = connection.search(
            search_base=base_dn, search_filter="(objectClass=*)", search_scope=BASE, size_limit=1, attributes=["1.1"]
        )

        if not success:
            demisto.info(f"Base DN verification failed. Result: {connection.result}")
            return False

        return True

    except Exception as e:
        demisto.error(f"Error during Base DN verification: {e}\n{traceback.format_exc()}")
        return False


def generate_unique_cn(default_base_dn, cn):
    changing_cn = cn
    i = 1
    while check_if_user_exists_by_attribute(default_base_dn, "cn", changing_cn):
        changing_cn = cn + str(i)
        i += 1
        if i == 30:
            raise Exception("User CN couldn't be generated")
    return changing_cn


def generate_dn_and_remove_from_user_profile(default_base_dn, user):
    """Generates a user dn, in case user dn is included in the user, will return it, otherwise
    will generate one using the cn and ou values
    :param default_base_dn: The location in the DIT where the search will start
    :param user: The user dict including his values
    :return: The user's dn.
    """
    user_dn = user.get("dn")

    if user_dn:
        user.pop("dn")
        return user_dn
    user_cn = user.get("cn")
    if not user_cn:
        raise Exception("User must have cn, please provide a valid value")

    valid_cn = generate_unique_cn(default_base_dn, user.get("cn"))
    ou = user.get("ou")

    return "CN=" + str(valid_cn) + "," + str(ou)


def check_if_user_exists_by_attribute(default_base_dn, attr, val):
    """Check if user exists base on a specific attribute
    :param default_base_dn: The location in the DIT where the search will start
    :param attr: The attribute to search by
    :param val: The attribute's value
    :return: True if the user exists, False otherwise.
    """
    query = f"(&(objectClass=User)(objectCategory=person)({attr}={val}))"
    entries = search_with_paging(query, default_base_dn, attributes=[attr], size_limit=1, page_size=1)
    return bool(entries.get("flat"))


def get_user_activity_by_samaccountname(default_base_dn, samaccountname):
    """Get if user is active or not by samaccountname
    :param default_base_dn: The location in the DIT where the search will start
    :param samaccountname: The user's unique samaccountname
    :return: True if the user active, False otherwise.
    """
    active = False
    query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={samaccountname}))"
    entries = search_with_paging(query, default_base_dn, attributes=["userAccountControl"], size_limit=1, page_size=1)

    if entries.get("flat"):
        user = entries.get("flat")[0]
        activity = user.get("userAccountControl")[0]
        active = activity not in INACTIVE_LIST_OPTIONS

    return active


def get_user_dn_by_email(default_base_dn, email):
    """Get's user dn by it's email, this function assumes that user's unique sameaccountname it the email prefix
    :param default_base_dn: The location in the DIT where the search will start
    :param email: The user's email
    :return: the user's dn
    """
    dn = ""
    samaccountname = email.split("@")[0]
    query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={samaccountname}))"
    entries = search_with_paging(query, default_base_dn, attributes=["sAMAccountName"], size_limit=1, page_size=1)

    if entries.get("flat"):
        user = entries.get("flat")[0]
        dn = user.get("dn")

    return dn


def modify_user_ou(dn, new_ou):
    assert connection is not None
    cn = dn.split(",OU=", 1)[0]
    cn = cn.split(",DC=", 1)[0]
    # removing // to fix customers bug
    cn = cn.replace("\\", "")
    dn = dn.replace("\\", "")

    success = connection.modify_dn(dn, cn, new_superior=new_ou)
    return success


def get_all_attributes(search_base):
    obj_inetorgperson = ObjectDef("user", connection)
    r = Reader(connection, obj_inetorgperson, search_base)
    r.search()
    if not r:
        return []
    if not r[0]:
        return []
    attributes = r[0].allowedAttributes
    return list(attributes)


""" COMMANDS """

""" SEARCH """


def search(search_filter, search_base, attributes=None, size_limit=0, time_limit=0):
    """
    find entries in the DIT

    Args:
        search_base: the location in the DIT where the search will start
        search_filter: LDAP query string
        attributes: the attributes to specify for each entry found in the DIT

    """
    assert connection is not None
    demisto.debug(f"searching using {search_filter=} {search_base=}")
    success = connection.search(
        search_base=search_base, search_filter=search_filter, attributes=attributes, size_limit=size_limit, time_limit=time_limit
    )

    if not success:
        demisto.info("Search failed")
        raise Exception("Search failed")
    return connection.entries


def search_with_paging(search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0, page_cookie=None):
    """
    find entries in the DIT

    Args:
        search_base: the location in the DIT where the search will start
        search_filter: LDAP query string
        attributes: the attributes to specify for each entry found in the DIT
    """
    assert connection is not None
    total_entries = 0
    cookie = base64.b64decode(page_cookie) if page_cookie else None
    start = datetime.now()

    entries: list[Entry] = []
    entries_left_to_fetch = size_limit
    page_num_debug = 0
    while True:
        page_num_debug += 1
        if 0 < entries_left_to_fetch < page_size:
            page_size = entries_left_to_fetch
        demisto.debug(
            f"search_with_paging: fetching page {page_num_debug}. "
            f"{page_size=}, total_so_far={total_entries} "
            f"has_cookie={bool(cookie)}, elapsed={(datetime.now() - start).total_seconds():.2f}s"
        )
        connection.search(
            search_base, search_filter, search_scope=SUBTREE, attributes=attributes, paged_size=page_size, paged_cookie=cookie
        )
        entries_left_to_fetch -= len(connection.entries)
        total_entries += len(connection.entries)
        cookie = dict_safe_get(connection.result, ["controls", "1.2.840.113556.1.4.319", "value", "cookie"])
        time_diff = (datetime.now() - start).seconds

        entries.extend(connection.entries)

        # stop when: 1.reached size limit 2.reached time limit 3. no cookie
        if (size_limit and size_limit <= total_entries) or (time_limit and time_diff >= time_limit) or (not cookie):
            demisto.debug(
                f"search_with_paging: stopping after {page_num_debug} page(s). "
                f"total_entries={total_entries}, size_limit={size_limit}, time_limit={time_limit}, "
                f"time_elapsed={time_diff}s, has_more_pages={bool(cookie)}"
            )
            break

    # keep the raw entry for raw content (backward compatibility)
    raw = []
    # flatten the entries
    flat = []

    for entry in entries:
        entry = json.loads(entry.entry_to_json())

        flat_entry = {"dn": entry["dn"]}

        for attr in entry.get("attributes", {}):
            flat_entry[attr] = entry["attributes"][attr]

        raw.append(entry)
        flat.append(flat_entry)
    encode_cookie = b64_encode(cookie) if cookie else None
    return {"raw": raw, "flat": flat, "page_cookie": encode_cookie}


def user_dn(sam_account_name, search_base):
    if "\\" in sam_account_name:
        domain_throw_out, sam_account_name = sam_account_name.split("\\", 1)
        demisto.info(f"chopping off domain {domain_throw_out} and using {sam_account_name=}")

    search_filter = f"(&(objectClass=user)(sAMAccountName={sam_account_name}))"
    entries = search(search_filter, search_base)
    if not entries:
        raise Exception(f"Could not get full DN for user with sAMAccountName '{sam_account_name}'")
    entry = json.loads(entries[0].entry_to_json())
    return entry["dn"]


def computer_dn(compuer_name, search_base):
    search_filter = f"(&(objectClass=user)(objectCategory=computer)(name={compuer_name}))"
    entries = search(search_filter, search_base)
    if not entries:
        raise Exception(f"Could not get full DN for computer with name '{compuer_name}'")
    entry = json.loads(entries[0].entry_to_json())
    return entry["dn"]


def group_dn(group_name, search_base):
    group_name = escape_filter_chars(group_name)
    search_filter = f"(&(objectClass=group)(cn={group_name}))"
    entries = search(search_filter, search_base)
    if not entries:
        raise Exception(f"Could not get full DN for group with name '{group_name}'")
    entry = json.loads(entries[0].entry_to_json())
    return entry["dn"]


def convert_special_chars_to_unicode(search_filter):
    # We allow users to use special chars without explicitly typing their unicode values
    chars_to_replace = {"\\(": "\\28", "\\)": "\\29", "\\*": "\\2a", "\\/": "\\2f", "\\\\": "\\5c"}
    for i, j in chars_to_replace.items():
        search_filter = search_filter.replace(i, j)

    return search_filter


def free_search(default_base_dn, page_size):
    args = demisto.args()

    search_filter = args.get("filter")
    size_limit = int(args.get("size-limit", "0"))
    time_limit = int(args.get("time-limit", "0"))
    search_base = args.get("base-dn") or default_base_dn
    attributes = args.get("attributes")
    context_output = args.get("context-output")

    search_filter = convert_special_chars_to_unicode(search_filter)

    # if ALL was specified - get all the object's attributes, else expect a string of comma separated values
    if attributes:
        attributes = ALL_ATTRIBUTES if attributes == "ALL" else attributes.split(",")

    page_cookie = args.get("page-cookie")
    if args.get("page-size"):
        page_size = arg_to_number(args["page-size"])
        size_limit = page_size

    entries = search_with_paging(
        search_filter,
        search_base,
        attributes=attributes,
        size_limit=size_limit,
        time_limit=time_limit,
        page_size=page_size,
        page_cookie=page_cookie,
    )
    ec = (
        {}
        if context_output == "no"
        else {
            "ActiveDirectory.Search(obj.dn == val.dn)": entries["flat"],
            "ActiveDirectory(true)": {"SearchPageCookie": entries["page_cookie"]},
        }
    )
    demisto_entry = {
        "ContentsFormat": formats["json"],
        "Type": entryTypes["note"],
        "Contents": entries["raw"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": tableToMarkdown("Active Directory Search", entries["flat"]),
        "EntryContext": ec,
    }
    demisto.results(demisto_entry)


def search_users(default_base_dn, page_size):
    # this command is equivalent to script ADGetUser
    # will preform a custom search to find users by a specific (one) attribute specified by the user

    args = demisto.args()
    params = demisto.params()
    attributes: list[str] = []
    custom_attributes: list[str] = []

    # zero is actually no limitation, default is 20
    limit = int(args.get("limit", "20"))
    if limit <= 0:
        limit = 20

    page_cookie = args.get("page-cookie")
    if args.get("page-size"):
        page_size = arg_to_number(args["page-size"])
        limit = page_size

    # getting default query from parameter - list all users
    query = params.get("default_base_query", "(&(objectClass=User)(objectCategory=person))")
    # query by user DN
    if args.get("dn"):
        dn = escape_filter_chars(args["dn"])
        query = f"(&(objectClass=User)(objectCategory=person)(distinguishedName={dn}))"

    # query by name
    if args.get("name"):
        name = escape_filter_chars(args["name"])
        query = f"(&(objectClass=User)(objectCategory=person)(cn={name}))"

    # query by email
    if args.get("email"):
        email = escape_filter_chars(args["email"])
        query = f"(&(objectClass=User)(objectCategory=person)(mail={email}))"

    # query by sAMAccountName
    if args.get("username") or args.get("sAMAccountName"):
        username = escape_filter_chars(args["username"]) if args.get("username") else escape_filter_chars(args["sAMAccountName"])
        query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={username}))"

    # query by custom object attribute
    if args.get("custom-field-type"):
        if not args.get("custom-field-data"):
            raise Exception('Please specify "custom-field-data" as well when quering by "custom-field-type"')
        field_type = escape_filter_chars(args["custom-field-type"])
        field_data = escape_filter_chars(args["custom-field-data"])
        query = f"(&(objectClass=User)(objectCategory=person)({field_type}={field_data}))"

    if args.get("attributes"):
        custom_attributes = args["attributes"].split(",")

    attributes = list(set(custom_attributes + DEFAULT_PERSON_ATTRIBUTES) - set(argToList(args.get("attributes-to-exclude"))))
    if "userAccountControl" in attributes:
        attributes.append("msDS-User-Account-Control-Computed")

    demisto.debug(
        f"ad-get-user: starting search_with_paging. "
        f"{query=}, {default_base_dn=}, {limit=}, {page_size=}, attributes={attributes}"
    )
    entries = search_with_paging(
        query, default_base_dn, page_cookie=page_cookie, attributes=attributes, size_limit=limit, page_size=page_size
    )
    demisto.debug(f"ad-get-user: search_with_paging completed.\nReturned {len(entries.get('flat', []))} entries.")

    accounts = [account_entry(entry, custom_attributes) for entry in entries["flat"]]
    if "userAccountControl" in attributes:
        for user in entries["flat"]:
            if user.get("userAccountControl"):
                user_account_control = user.get("userAccountControl")[0]
                user["userAccountControlFields"] = user_account_to_boolean_fields(user_account_control)

                # display a literal translation of the numeric account control flag
                if args.get("user-account-control-out", "") == "true":
                    user["userAccountControl"] = COMMON_ACCOUNT_CONTROL_FLAGS.get(user_account_control) or user_account_control

            if user.get("msDS-User-Account-Control-Computed"):
                user_account_control_msDS = user.get("msDS-User-Account-Control-Computed")[0]
                user_account_to_boolean_dict = user_account_to_boolean_fields_msDS_user_account_control_computed(
                    user_account_control_msDS
                )
                user.setdefault("userAccountControlFields", {}).update(user_account_to_boolean_dict)

    entry_context = {
        "ActiveDirectory.Users(obj.dn == val.dn)": entries["flat"],
        # 'backward compatability' with ADGetUser script
        "Account(obj.ID == val.ID)": accounts,
        "ActiveDirectory(true)": {"UsersPageCookie": entries["page_cookie"]} if entries["page_cookie"] else None,
    }
    remove_nulls_from_dictionary(entry_context)

    demisto_entry = {
        "ContentsFormat": formats["json"],
        "Type": entryTypes["note"],
        "Contents": entries["raw"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": tableToMarkdown("Active Directory - Get Users", entries["flat"]),
        "EntryContext": entry_context,
    }
    demisto.results(demisto_entry)


def get_user_iam(default_base_dn, args, mapper_in, mapper_out):
    """Gets an AD user by User Profile.
    :param default_base_dn: The location in the DIT where the search will start
    :param args: Demisto args.
    :param mapper_in: Mapping AD user to User Profiles
    :param mapper_out: Mapping User Profiles to AD users.
    :return: User Profile of the AD user
    """
    try:
        user_profile = args.get("user-profile")
        user_profile_delta = args.get("user-profile-delta")
        default_attribute = "sAMAccountName"

        iam_user_profile = IAMUserProfile(
            user_profile=user_profile,
            user_profile_delta=user_profile_delta,
            mapper=mapper_out,
            incident_type=IAMUserProfile.UPDATE_INCIDENT_TYPE,
        )

        # we use the outgoing mapper to get all the AD attributes which will be later passed to search_with_paging()
        ad_user = iam_user_profile.map_object(mapper_name=mapper_out, incident_type=IAMUserProfile.CREATE_INCIDENT_TYPE)

        value = ad_user.get(default_attribute)

        # removing keys with no values
        user = {k: v for k, v in ad_user.items() if v}
        attributes = list(user.keys())

        query = f"(&(objectClass=User)(objectCategory=person)({default_attribute}={value}))"
        entries = search_with_paging(query, default_base_dn, attributes=attributes, size_limit=1, page_size=1)

        if not entries.get("flat"):
            error_code, error_message = IAMErrors.USER_DOES_NOT_EXIST
            iam_user_profile.set_result(
                action=IAMActions.GET_USER, success=False, error_code=error_code, error_message=error_message
            )
        else:
            user_account_control = get_user_activity_by_samaccountname(default_base_dn, value)
            ad_user["userAccountControl"] = user_account_control
            iam_user_profile.update_with_app_data(ad_user, mapper_in)
            iam_user_profile.set_result(
                success=True,
                email=ad_user.get("email"),
                username=ad_user.get("name"),
                action=IAMActions.GET_USER,
                details=ad_user,
                active=user_account_control,
            )

        return iam_user_profile

    except Exception as e:
        error_code, _ = IAMErrors.BAD_REQUEST
        iam_user_profile.set_result(success=False, error_code=error_code, error_message=str(e), action=IAMActions.GET_USER)
        return iam_user_profile


def search_computers(default_base_dn, page_size):
    # this command is equivalent to ADGetComputer script

    args = demisto.args()
    attributes: list[str] = []
    custom_attributes: list[str] = []

    # default query - list all users (computer category)
    query = "(&(objectClass=user)(objectCategory=computer))"

    # query by user DN
    if args.get("dn"):
        query = "(&(objectClass=user)(objectCategory=computer)(distinguishedName={}))".format(args["dn"])

    # query by name
    if args.get("name"):
        query = "(&(objectClass=user)(objectCategory=computer)(name={}))".format(args["name"])

    # query by custom object attribute
    if args.get("custom-field-type"):
        if not args.get("custom-field-data"):
            raise Exception('Please specify "custom-field-data" as well when quering by "custom-field-type"')
        query = "(&(objectClass=user)(objectCategory=computer)({}={}))".format(
            args["custom-field-type"], args["custom-field-data"]
        )

    size_limit = int(args.get("limit", "0"))
    page_cookie = args.get("page-cookie")
    if args.get("page-size"):
        page_size = arg_to_number(args["page-size"])
        size_limit = page_size

    if args.get("attributes"):
        custom_attributes = args["attributes"].split(",")
    attributes = list(set(custom_attributes + DEFAULT_COMPUTER_ATTRIBUTES))

    demisto.debug(
        f"ad-get-computer: starting search_with_paging. "
        f"{query=}, {default_base_dn=}, {size_limit=}, {page_size=}, attributes={attributes}"
    )
    entries = search_with_paging(
        query, default_base_dn, attributes=attributes, page_size=page_size, size_limit=size_limit, page_cookie=page_cookie
    )
    demisto.debug(f"ad-get-computer: search_with_paging completed.\nReturned {len(entries.get('flat', []))} entries.")

    endpoints = [endpoint_entry(entry, custom_attributes) for entry in entries["flat"]]
    readable_output = tableToMarkdown("Active Directory - Get Computers", entries["flat"])

    if endpoints:
        results = CommandResults(
            readable_output=readable_output,
            outputs={
                "ActiveDirectory.Computers(obj.dn == val.dn)": entries["flat"],
                # 'backward compatability' with ADGetComputer script
                "Endpoint(obj.ID == val.ID)": endpoints,
                "ActiveDirectory(true)": {"ComputersPageCookie": entries["page_cookie"]},
            },
            raw_response=entries["raw"],
        )
    else:
        results = CommandResults(
            readable_output=readable_output,
        )

    return_results(results)


def search_group_members(default_base_dn, page_size):
    # this command is equivalent to ADGetGroupMembers script

    args = demisto.args()
    member_type = args.get("member-type")
    group_dn = args.get("group-dn")
    nested_search = "" if args.get("disable-nested-search") == "true" else ":1.2.840.113556.1.4.1941:"
    time_limit = int(args.get("time_limit", 180))
    account_name = args.get("sAMAccountName")
    custom_attributes: list[str] = []

    default_attribute_mapping = {
        "person": DEFAULT_PERSON_ATTRIBUTES,
        "group": DEFAULT_GROUP_ATTRIBUTES,
        "computer": DEFAULT_COMPUTER_ATTRIBUTES,
    }
    default_attributes = default_attribute_mapping.get(member_type, DEFAULT_COMPUTER_ATTRIBUTES)

    if args.get("attributes"):
        custom_attributes = args["attributes"].split(",")

    attributes = list(set(custom_attributes + default_attributes))

    if member_type == "group":
        query = f"(&(objectCategory={member_type})(memberOf{nested_search}={group_dn})(sAMAccountName={account_name}))"
    else:
        query = (
            f"( & (objectCategory={member_type})(objectClass=user)"
            f"(memberOf{nested_search}={group_dn})(sAMAccountName={account_name}))"
        )

    size_limit = int(args.get("limit", "0"))
    page_cookie = args.get("page-cookie")
    if args.get("page-size"):
        page_size = arg_to_number(args["page-size"])
        size_limit = page_size

    entries = search_with_paging(
        query,
        default_base_dn,
        attributes=attributes,
        page_size=page_size,
        time_limit=time_limit,
        size_limit=size_limit,
        page_cookie=page_cookie,
    )
    members = [{"dn": entry["dn"], "category": member_type} for entry in entries["flat"]]
    demisto_entry = {
        "ContentsFormat": formats["json"],
        "Type": entryTypes["note"],
        "Contents": entries["raw"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": tableToMarkdown("Active Directory - Get Group Members", entries["flat"]),
        "EntryContext": {
            "ActiveDirectory.Groups(obj.dn ==" + group_dn + ")": {"dn": group_dn, "members": members},
            "ActiveDirectory(true)": {"GroupsPageCookie": entries["page_cookie"]},
        },
    }

    if member_type == "person":
        demisto_entry["EntryContext"]["ActiveDirectory.Users(obj.dn == val.dn)"] = entries["flat"]
        demisto_entry["EntryContext"]["Account"] = [account_entry(entry, custom_attributes) for entry in entries["flat"]]
    elif member_type == "computer":
        demisto_entry["EntryContext"]["ActiveDirectory.Computers(obj.dn == val.dn)"] = entries["flat"]
        demisto_entry["EntryContext"]["Endpoint"] = [endpoint_entry(entry, custom_attributes) for entry in entries["flat"]]
    elif member_type == "group":
        demisto_entry["EntryContext"]["ActiveDirectory.Groups(obj.dn == val.dn)"] = entries["flat"]
        demisto_entry["EntryContext"]["Group"] = [group_entry(entry, custom_attributes) for entry in entries["flat"]]

    demisto.results(demisto_entry)


""" DATABASE OPERATIONS """

""" CREATE OBJECT"""


def create_user():
    assert connection is not None
    args = demisto.args()

    object_classes = ["top", "person", "organizationalPerson", "user"]
    user_dn = args.get("user-dn")
    username = args.get("username")
    password = args.get("password")
    custom_attributes = args.get("custom-attributes")
    attributes = {"sAMAccountName": username}

    # set common user attributes
    if args.get("display-name"):
        attributes["displayName"] = args["display-name"]
    if args.get("description"):
        attributes["description"] = args["description"]
    if args.get("email"):
        attributes["mail"] = args["email"]
    if args.get("telephone-number"):
        attributes["telephoneNumber"] = args["telephone-number"]
    if args.get("title"):
        attributes["title"] = args["title"]

    # set user custom attributes
    if custom_attributes:
        try:
            custom_attributes = json.loads(custom_attributes)
        except Exception as e:
            demisto.info(str(e))
            raise Exception(
                "Failed to parse custom attributes argument. Please see an example of this argument in the description."
            )
        for attribute_name, attribute_value in custom_attributes.items():
            # can run default attribute setting
            attributes[attribute_name] = attribute_value

    # add user
    success = connection.add(user_dn, object_classes, attributes)
    if not success:
        raise Exception("Failed to create user")

    # set user password
    success = connection.extend.microsoft.modify_password(user_dn, password)
    if not success:
        raise Exception("Failed to reset user password")

    # enable user and expire password
    modification = {
        # enable user
        "userAccountControl": [("MODIFY_REPLACE", NORMAL_ACCOUNT)],
        # set to 0, to force password change on next login
        "pwdLastSet": [("MODIFY_REPLACE", "0")],
    }
    modify_object(user_dn, modification)

    demisto_entry = {
        "ContentsFormat": formats["text"],
        "Type": entryTypes["note"],
        "Contents": f"Created user with DN: {user_dn}",
    }
    demisto.results(demisto_entry)


def create_user_iam(default_base_dn, args, mapper_out, disabled_users_group_cn):
    """Creates an AD user by User Profile.
    :param default_base_dn: The location in the DIT where the search will start
    :param args: Demisto args.
    :param mapper_out: Mapping User Profiles to AD users.
    :param disabled_users_group_cn: The disabled group cn, the user will be removed from this group when enabled
    :return: The user that was created
    """
    assert connection is not None
    try:
        user_profile = args.get("user-profile")
        user_profile_delta = args.get("user-profile-delta")
        iam_user_profile = IAMUserProfile(
            user_profile=user_profile,
            user_profile_delta=user_profile_delta,
            mapper=mapper_out,
            incident_type=IAMUserProfile.CREATE_INCIDENT_TYPE,
        )
        ad_user = iam_user_profile.map_object(mapper_name=mapper_out, incident_type=IAMUserProfile.CREATE_INCIDENT_TYPE)

        sam_account_name = ad_user.get("sAMAccountName")

        if not sam_account_name:
            raise DemistoException(
                'User must have a sAMAccountName, please make sure a mapping exists in "' + mapper_out + '" outgoing mapper.'
            )
        if not ad_user.get("ou"):
            raise DemistoException(
                "User must have an Organizational Unit (OU). Please make sure you've added a "
                "transformer script which determines the OU of the user "
                'in "' + mapper_out + '" outgoing mapper, in the User Profile incident type '
                'and schema type, under the "ou" field.'
            )

        user_exists = check_if_user_exists_by_attribute(default_base_dn, "sAMAccountName", sam_account_name)

        if user_exists:
            iam_user_profile = update_user_iam(default_base_dn, args, False, mapper_out, disabled_users_group_cn)

        else:
            user_dn = generate_dn_and_remove_from_user_profile(default_base_dn, ad_user)
            object_classes = ["top", "person", "organizationalPerson", "user"]
            # ou and cn are updated from the dn, updating them seperatly can cause conflicts
            ad_user.pop("ou")
            ad_user.pop("cn")
            if manager_email := ad_user.get("manageremail"):
                manager_dn = get_user_dn_by_email(default_base_dn, manager_email)
                ad_user["manager"] = manager_dn
            success = connection.add(user_dn, object_classes, ad_user)
            if success:
                iam_user_profile.set_result(
                    success=True,
                    email=ad_user.get("mail"),
                    username=ad_user.get("sAMAccountName"),
                    details=ad_user,
                    action=IAMActions.CREATE_USER,
                    active=False,
                )  # the user should be activated with the IAMInitADUser script

            else:
                error_msg = (
                    "Please validate your instance configuration and make sure all of the "
                    'required attributes are mapped correctly in "' + mapper_out + '" outgoing mapper.'
                )
                raise DemistoException(error_msg)

        return iam_user_profile

    except Exception as e:
        error_code, _ = IAMErrors.BAD_REQUEST
        iam_user_profile.set_result(
            success=False,
            error_code=error_code,
            error_message=str(e),
            action=IAMActions.CREATE_USER,
        )
        return iam_user_profile


def get_iam_user_profile(user_profile, mapper_out):
    iam_user_profile = IAMUserProfile(
        user_profile=user_profile, mapper=mapper_out, incident_type=IAMUserProfile.UPDATE_INCIDENT_TYPE
    )
    ad_user = iam_user_profile.map_object(mapper_name=mapper_out, incident_type=IAMUserProfile.UPDATE_INCIDENT_TYPE)
    sam_account_name = ad_user.get("sAMAccountName")

    old_user_data = iam_user_profile.get_attribute("olduserdata")
    if old_user_data:
        iam_old_user_profile = IAMUserProfile(
            user_profile=old_user_data, mapper=mapper_out, incident_type=IAMUserProfile.UPDATE_INCIDENT_TYPE
        )
        ad_old_user = iam_old_user_profile.map_object(mapper_name=mapper_out, incident_type=IAMUserProfile.UPDATE_INCIDENT_TYPE)
        sam_account_name = ad_old_user.get("sAMAccountName") or sam_account_name

    return iam_user_profile, ad_user, sam_account_name


def update_user_iam(default_base_dn, args, create_if_not_exists, mapper_out, disabled_users_group_cn):
    """Update an AD user by User Profile.
    :param default_base_dn: The location in the DIT where the search will start
    :param args: Demisto args.
    :param create_if_not_exists: Created the user if it does not exists.
    :param mapper_out: Mapping User Profiles to AD users.
    :param disabled_users_group_cn: The disabled group cn, the user will be removed from this group when enabled
    :return: Updated User
    """
    assert connection is not None
    try:
        user_profile = args.get("user-profile")
        allow_enable = args.get("allow-enable") == "true"

        iam_user_profile, ad_user, sam_account_name = get_iam_user_profile(user_profile, mapper_out)

        if not sam_account_name:
            raise DemistoException(
                'User must have a sAMAccountName, please make sure a mapping exists in "' + mapper_out + '" outgoing mapper.'
            )
        if not ad_user.get("ou"):
            raise DemistoException(
                "User must have an Organizational Unit (OU). Please make sure you've added a "
                "transformer script which determines the OU of the user "
                'in "' + mapper_out + '" outgoing mapper, in the User Profile incident type '
                'and schema type, under the "ou" field.'
            )

        new_ou = ad_user.get("ou")
        user_exists = check_if_user_exists_by_attribute(default_base_dn, "sAMAccountName", sam_account_name)

        if not user_exists:
            if create_if_not_exists:
                iam_user_profile = create_user_iam(default_base_dn, args, mapper_out, disabled_users_group_cn)
            else:
                _, error_message = IAMErrors.USER_DOES_NOT_EXIST
                iam_user_profile.set_result(action=IAMActions.UPDATE_USER, skip=True, skip_reason=error_message)
        else:
            dn = user_dn(sam_account_name, default_base_dn)

            if allow_enable:
                enable_user_iam(default_base_dn, dn, disabled_users_group_cn)

            # fields that can't be modified
            # notice that we are changing the ou and that effects the dn and cn
            for field in FIELDS_THAT_CANT_BE_MODIFIED:
                if ad_user.get(field):
                    ad_user.pop(field)

            fail_to_modify = []
            if manager_email := ad_user.get("manageremail"):
                manager_dn = get_user_dn_by_email(default_base_dn, manager_email)
                ad_user["manager"] = manager_dn
                ad_user.pop("manageremail")

            for key in ad_user:
                modification = {key: [("MODIFY_REPLACE", ad_user.get(key))]}
                success = connection.modify(dn, modification)
                if not success:
                    fail_to_modify.append(key)

            ou_modified_succeed = modify_user_ou(dn, new_ou)
            if not ou_modified_succeed:
                fail_to_modify.append("ou")

            if fail_to_modify:
                error_list = "\n".join(fail_to_modify)
                error_message = f"Failed to modify the following attributes: {error_list}"
                raise DemistoException(error_message)

            else:
                active = get_user_activity_by_samaccountname(default_base_dn, sam_account_name)
                iam_user_profile.set_result(
                    success=True,
                    email=ad_user.get("mail"),
                    username=ad_user.get("sAMAccountName"),
                    action=IAMActions.UPDATE_USER,
                    details=ad_user,
                    active=active,
                )
        return iam_user_profile

    except Exception as e:
        error_code, _ = IAMErrors.BAD_REQUEST
        iam_user_profile.set_result(success=False, error_code=error_code, error_message=str(e), action=IAMActions.UPDATE_USER)
        return iam_user_profile


def create_contact():
    assert connection is not None
    args = demisto.args()

    object_classes = ["top", "person", "organizationalPerson", "contact"]
    contact_dn = args.get("contact-dn")

    # set contact attributes
    attributes: dict = {}
    if args.get("custom-attributes"):
        try:
            attributes = json.loads(args["custom-attributes"])
        except Exception as e:
            demisto.info(str(e))
            raise Exception("Failed to parse custom attributes argument. Please see an example of this argument in the argument.")

    # set common user attributes
    if args.get("display-name"):
        attributes["displayName"] = args["display-name"]
    if args.get("description"):
        attributes["description"] = args["description"]
    if args.get("email"):
        attributes["mail"] = args["email"]
    if args.get("telephone-number"):
        attributes["telephoneNumber"] = args["telephone-number"]
    if args.get("title"):
        attributes["title"] = args["title"]

    # add contact

    success = connection.add(contact_dn, object_classes, attributes)
    if not success:
        raise Exception("Failed to create contact")

    demisto_entry = {
        "ContentsFormat": formats["text"],
        "Type": entryTypes["note"],
        "Contents": f"Created contact with DN: {contact_dn}",
    }
    demisto.results(demisto_entry)


def create_group():
    assert connection is not None
    args = demisto.args()

    object_classes = ["top", "group"]
    dn = args.get("dn")
    group_name = args.get("name")
    group_type_map = {"security": "2147483650", "distribution": "2"}
    group_type = group_type_map[args.get("group-type")]
    if args.get("members"):
        members = args.get("members")
        attributes = {"samAccountName": group_name, "groupType": group_type, "member": members}
    else:
        attributes = {"samAccountName": group_name, "groupType": group_type}

    # create group
    success = connection.add(dn, object_classes, attributes)
    if not success:
        raise Exception("Failed to create group")

    demisto_entry = {"ContentsFormat": formats["text"], "Type": entryTypes["note"], "Contents": f"Created group with DN: {dn}"}
    demisto.results(demisto_entry)


""" UPDATE OBJECT """


def prepare_attribute_value(attribute_value: str | None, attribute_type: str | None) -> bytes | str | None:
    """
    Converts an attribute value string to the appropriate Python type for the LDAP modify call.

    When *attribute_type* is ``"byte"``, the value is interpreted as a comma-separated list of
    decimal integers (0-255) and converted to a raw :class:`bytes` object so that ldap3 sends a
    proper Octet String over the wire.  This is required for binary AD attributes such as
    ``logonHours``, ``objectSID``, and ``objectGUID``.

    For all other attribute types the value is returned unchanged as a plain string.

    :param attribute_value: The raw string value supplied by the caller.
    :param attribute_type: Optional type hint.  Pass ``"byte"`` for binary/Octet String attributes.
    :return: A :class:`bytes` object for binary attributes, or the original string otherwise.
    :raises ValueError: If *attribute_type* is ``"byte"`` but the value cannot be parsed as a
        comma-separated list of integers in the range 0-255.
    """
    if not (attribute_type and attribute_type.lower() == "byte"):
        demisto.debug(f"attribute-type argument is not 'byte', returning value as string: {attribute_value}")
        return attribute_value

    if not attribute_value:
        raise DemistoException("attribute-value must be provided when attribute-type is 'byte'.")
    try:
        byte_values = argToList(attribute_value, transform=int)
    except ValueError:
        raise DemistoException(
            f"attribute-value '{attribute_value}' cannot be parsed as a comma-separated list of integers. "
            "When attribute-type is 'byte', provide values as comma-separated decimal integers, "
            "e.g. '0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0'."
        )
    if any(b < 0 or b > 255 for b in byte_values):
        raise DemistoException(
            f"attribute-value contains out-of-range integers. Each byte must be between 0 and 255, got: {byte_values}"
        )
    demisto.debug(f"attribute-type is 'byte', returning value as bytes: {byte_values}")
    return bytes(byte_values)


def modify_object(dn, modification):
    """
    modifies object in the DIT
    """
    assert connection is not None
    success = connection.modify(dn, modification)
    if not success:
        raise Exception(f"Failed to update object {dn} with the following modification: {json.dumps(modification)}")


def update_user(default_base_dn):
    args = demisto.args()

    # get user DN
    sam_account_name = args.get("username")
    attribute_name = args.get("attribute-name")
    attribute_value = args.get("attribute-value")
    attribute_type = args.get("attribute-type")
    search_base = args.get("base-dn") or default_base_dn
    dn = user_dn(sam_account_name, search_base)

    modification = {}
    modification[attribute_name] = [("MODIFY_REPLACE", prepare_attribute_value(attribute_value, attribute_type))]

    # modify user
    modify_object(dn, modification)

    demisto_entry = {
        "ContentsFormat": formats["text"],
        "Type": entryTypes["note"],
        "Contents": f"Updated user's {attribute_name} to {attribute_value} ",
    }
    demisto.results(demisto_entry)


def update_group(default_base_dn):
    args = demisto.args()

    sam_account_name = args.get("groupname")
    attribute_name = args.get("attributename")
    attribute_value = args.get("attributevalue")
    attribute_type = args.get("attribute-type")
    search_base = args.get("basedn") or default_base_dn
    dn = group_dn(sam_account_name, search_base)

    modification = {attribute_name: [("MODIFY_REPLACE", prepare_attribute_value(attribute_value, attribute_type))]}
    modify_object(dn, modification)

    demisto_entry = {
        "ContentsFormat": formats["text"],
        "Type": entryTypes["note"],
        "Contents": f"Updated group's {attribute_name} to {attribute_value} ",
    }
    demisto.results(demisto_entry)


def update_contact():
    args = demisto.args()

    contact_dn = args.get("contact-dn")
    attribute_name = args.get("attribute-name")
    attribute_value = args.get("attribute-value")
    attribute_type = args.get("attribute-type")
    modification = {}
    modification[attribute_name] = [("MODIFY_REPLACE", prepare_attribute_value(attribute_value, attribute_type))]

    # modify
    modify_object(contact_dn, modification)

    demisto_entry = {
        "ContentsFormat": formats["text"],
        "Type": entryTypes["note"],
        "Contents": f"Updated contact's {attribute_name} to: {attribute_value} ",
    }
    demisto.results(demisto_entry)


def modify_computer_ou(default_base_dn):
    assert connection is not None
    args = demisto.args()

    computer_name = args.get("computer-name")
    dn = computer_dn(computer_name, args.get("base-dn") or default_base_dn)

    success = connection.modify_dn(dn, f"CN={computer_name}", new_superior=args.get("full-superior-dn"))
    if not success:
        raise Exception("Failed to modify computer OU")

    demisto_entry = {
        "ContentsFormat": formats["text"],
        "Type": entryTypes["note"],
        "Contents": "Moved computer {} to {}".format(computer_name, args.get("full-superior-dn")),
    }
    demisto.results(demisto_entry)


def modify_user_ou_command(default_base_dn):
    assert connection is not None
    args = demisto.args()

    user_name = args.get("user-name")
    dn = user_dn(user_name, args.get("base-dn") or default_base_dn)
    success = modify_user_ou(dn, new_ou=args.get("full-superior-dn"))
    if not success:
        raise Exception("Failed to modify user OU")

    return f'Moved user {user_name} to {args.get("full-superior-dn")}'


def expire_user_password(default_base_dn):
    args = demisto.args()

    # get user DN
    sam_account_name = args.get("username")
    search_base = args.get("base-dn") or default_base_dn
    dn = user_dn(sam_account_name, search_base)

    modification = {
        # set to 0, to force password change on next login
        "pwdLastSet": [("MODIFY_REPLACE", "0")]
    }

    # modify user
    modify_object(dn, modification)

    demisto_entry = {"ContentsFormat": formats["text"], "Type": entryTypes["note"], "Contents": "Expired password successfully"}
    demisto.results(demisto_entry)


def set_user_password(default_base_dn, port):
    assert connection is not None
    args = demisto.args()

    if port != 636:
        raise DemistoException("Port 636 is required for this action.")

    # get user DN
    sam_account_name = args.get("username")
    password = args.get("password")
    search_base = args.get("base-dn") or default_base_dn
    dn = user_dn(sam_account_name, search_base)

    # set user password
    success = connection.extend.microsoft.modify_password(dn, password)
    if not success:
        raise Exception("Failed to reset user password")

    demisto_entry = {"ContentsFormat": formats["text"], "Type": entryTypes["note"], "Contents": "User password successfully set"}
    demisto.results(demisto_entry)


def restore_user(default_base_dn: str, page_size: int) -> int:
    """
    Restore the user UserAccountControl flags.
    Args:
        default_base_dn (str): The default base dn.
        page_size (int): The page size to query.
    Returns:
        flags (int): The UserAccountControl flags.
    """
    args = demisto.args()

    # default query - list all users
    query = "(&(objectClass=User)(objectCategory=person))"

    # query by sAMAccountName
    if args.get("username") or args.get("sAMAccountName"):
        username = escape_filter_chars(args["username"]) if args.get("username") else escape_filter_chars(args["sAMAccountName"])
        query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={username}))"

    attributes = list(set(DEFAULT_PERSON_ATTRIBUTES))

    entries = search_with_paging(query, default_base_dn, attributes=attributes, size_limit=0, page_size=page_size)
    if entries.get("flat"):
        return entries.get("flat")[0].get("userAccountControl")[0]
    return 0


def turn_disable_flag_off(flags: int) -> int:
    """
    Turn off the ACCOUNTDISABLE flag in UserAccountControl flags.
    https://docs.microsoft.com/en-US/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties
     Args:
         flags (int): The UserAccountControl flags to update.
     Returns:
         flags (int): The UserAccountControl flags with the ACCOUNTDISABLE turned off.
    """
    return flags & ~(1 << (2 - 1))


def enable_user(default_base_dn, default_page_size):
    args = demisto.args()
    account_options = NORMAL_ACCOUNT
    # get user DN
    sam_account_name = args.get("username")
    search_base = args.get("base-dn") or default_base_dn
    dn = user_dn(sam_account_name, search_base)

    if args.get("restore_user"):
        account_options = restore_user(search_base, default_page_size)

    # modify user
    modification = {"userAccountControl": [("MODIFY_REPLACE", turn_disable_flag_off(account_options))]}
    modify_object(dn, modification)

    demisto_entry = {
        "ContentsFormat": formats["text"],
        "Type": entryTypes["note"],
        "Contents": f"User {sam_account_name} was enabled",
    }
    demisto.results(demisto_entry)


def disable_user(default_base_dn, default_page_size):
    args = demisto.args()

    # get user DN
    sam_account_name = args.get("username")
    search_base = args.get("base-dn") or default_base_dn
    dn = user_dn(sam_account_name, search_base)
    account_options = restore_user(search_base, default_page_size)

    # modify user
    modification = {"userAccountControl": [("MODIFY_REPLACE", (account_options | DISABLED_ACCOUNT))]}
    modify_object(dn, modification)

    demisto_entry = {
        "ContentsFormat": formats["text"],
        "Type": entryTypes["note"],
        "Contents": f"User {sam_account_name} was disabled",
    }
    demisto.results(demisto_entry)


def enable_user_iam(default_base_dn, dn, disabled_users_group_cn):
    """Enables an AD user by User Profile.
    :param default_base_dn: The location in the DIT where the search will start
    :param dn: The users unique dn
    :param disabled_users_group_cn: The disabled group cn, the user will be removed from this group when enabled
    """
    modification = {"userAccountControl": [("MODIFY_REPLACE", PASSWORD_NOT_REQUIRED)]}
    modify_object(dn, modification)
    if disabled_users_group_cn:
        grp_dn = group_dn(disabled_users_group_cn, default_base_dn)
        success = microsoft.removeMembersFromGroups.ad_remove_members_from_groups(connection, [dn], [grp_dn], True)
        if not success:
            raise Exception(f"Failed to remove user from {disabled_users_group_cn} group")


def disable_user_iam(default_base_dn, disabled_users_group_cn, args, mapper_out):
    """Disables an AD user by User Profile.
    :param default_base_dn: The location in the DIT where the search will start
    :param disabled_users_group_cn: The disabled group cn, the user will be added from this group when enabled
    :param args: Demisto args.
    :param mapper_out: Mapping User Profiles to AD users.
    :return: The disabled user
    """
    try:
        user_profile = args.get("user-profile")
        user_profile_delta = args.get("user-profile-delta")
        iam_user_profile = IAMUserProfile(
            user_profile=user_profile,
            user_profile_delta=user_profile_delta,
            mapper=mapper_out,
            incident_type=IAMUserProfile.UPDATE_INCIDENT_TYPE,
        )
        ad_user = iam_user_profile.map_object(mapper_name=mapper_out, incident_type=IAMUserProfile.UPDATE_INCIDENT_TYPE)

        sam_account_name = ad_user.get("sAMAccountName")
        if not sam_account_name:
            raise DemistoException(
                'User must have a sAMAccountName, please make sure a mapping exists in "' + mapper_out + '" outgoing mapper.'
            )

        user_exists = check_if_user_exists_by_attribute(default_base_dn, "sAMAccountName", sam_account_name)
        if not user_exists:
            iam_user_profile.set_result(success=True, action=IAMActions.DISABLE_USER, skip=True, skip_reason="User doesn't exist")
            return iam_user_profile

        dn = user_dn(sam_account_name, default_base_dn)

        # modify user
        modification = {"userAccountControl": [("MODIFY_REPLACE", DISABLED_ACCOUNT)]}

        try:
            modify_object(dn, modification)
        except Exception as e:
            error_msg = (
                "Please validate your instance configuration and make sure all of the "
                'required attributes are mapped correctly in "' + mapper_out + '" outgoing mapper.\n'
                "Error is: " + str(e)
            )
            raise DemistoException(error_msg)

        if disabled_users_group_cn:
            grp_dn = group_dn(disabled_users_group_cn, default_base_dn)
            success = microsoft.addMembersToGroups.ad_add_members_to_groups(connection, [dn], [grp_dn])
            if not success:
                raise DemistoException('Failed to remove user from the group "' + disabled_users_group_cn + '".')

        iam_user_profile.set_result(
            success=True,
            email=ad_user.get("mail"),
            username=ad_user.get("sAMAccountName"),
            action=IAMActions.DISABLE_USER,
            details=ad_user,
            active=False,
        )

        return iam_user_profile

    except Exception as e:
        error_code, _ = IAMErrors.BAD_REQUEST
        iam_user_profile.set_result(success=False, error_code=error_code, error_message=str(e), action=IAMActions.DISABLE_USER)
        return iam_user_profile


def add_member_to_group(default_base_dn):
    args = demisto.args()

    search_base = args.get("base-dn") or default_base_dn

    # get the  dn of the member - either user or computer
    args_err = "Please provide either username, computer-name, or nested_group_cn"
    member_dn = ""

    if args.get("username") and args.get("computer-name"):
        # both arguments passed
        raise Exception(args_err)
    if args.get("username"):
        usernames = argToList(args.get("username"))
        demisto.debug(f"Usernames collected are {usernames}")
        member_dns = []
        for u in usernames:
            member_dn = user_dn(u, search_base)
            demisto.debug(f"Member DNs after formatting are: {member_dn}")
            member_dns.append(member_dn)
    elif args.get("computer-name"):
        computers = argToList(args.get("computer-name"))
        member_dns = []
        for c in computers:
            member_dn = computer_dn(c, search_base)
            member_dns.append(member_dn)
    # added option to pass a Group CN to be added to the Group as a nested group
    elif args.get("nested_group_cn"):
        member_dn = group_dn(args["nested_group_cn"], search_base)
        member_dns = [member_dn]
    else:
        # none of the arguments passed
        raise Exception(args_err)

    grp_dn = group_dn(args.get("group-cn"), search_base)

    # Updated to take an array of member DNs to add to the group. Not detailed in the ldap3 documentation but per the function
    # hints https://github.com/cannatag/ldap3/blob/dev/ldap3/extend/microsoft/addMembersToGroups.py
    # def ad_add_members_to_groups(connection, members_nd, groups_dn, fixe=True, raise_error=False):
    # """
    # :param connection: a bound Connection object
    # :param members_dn: the list of members to add to groups
    # :param groups_dn: the list of groups where members are to be added
    # :param fix: checks for group existence and already assigned members
    # :param raise_error: If the operation fails it raises an error instead of returning False
    # :return: a boolean where True means that the operation was successful and False means an error has happened
    # Establishes users-groups relations following the Active Directory rules: users are added to the member attribute of groups.
    # Raises LDAPInvalidDnError if members or groups are not found in the DIT.
    # """
    success = microsoft.addMembersToGroups.ad_add_members_to_groups(
        connection=connection, members_dn=member_dns, groups_dn=[grp_dn], raise_error=True
    )
    demisto.debug(f"addMembersToGroups: {success}")
    if not success:
        raise Exception(success)

    demisto_entry = {
        "ContentsFormat": formats["text"],
        "Type": entryTypes["note"],
        "Contents": f"Object(s) with dn(s) {member_dns} were added to group {args.get('group-cn')}",
    }
    demisto.results(demisto_entry)


def remove_member_from_group(default_base_dn):
    args = demisto.args()

    search_base = args.get("base-dn") or default_base_dn

    # get the dn of the member - either user or computer
    args_err = "Pleade provide either username or computer-name"
    member_dn = ""

    if args.get("username") and args.get("computer-name"):
        # both arguments passed
        raise Exception(args_err)
    if args.get("username"):
        member_dn = user_dn(args["username"], search_base)
    elif args.get("computer-name"):
        member_dn = computer_dn(args["computer-name"], search_base)
    else:
        # none of the arguments passed
        raise Exception(args_err)

    grp_dn = group_dn(args.get("group-cn"), search_base)

    success = microsoft.removeMembersFromGroups.ad_remove_members_from_groups(connection, [member_dn], [grp_dn], True)
    if not success:
        raise Exception(
            "Failed to remove {} from group {}".format(args.get("username") or args.get("computer-name"), args.get("group-cn"))
        )

    demisto_entry = {
        "ContentsFormat": formats["text"],
        "Type": entryTypes["note"],
        "Contents": "Object with dn {} removed from group {}".format(member_dn, args.get("group-cn")),
    }
    demisto.results(demisto_entry)


def unlock_account(default_base_dn):
    args = demisto.args()

    # get user DN
    sam_account_name = args.get("username")
    search_base = args.get("base-dn") or default_base_dn
    dn = user_dn(sam_account_name, search_base)

    success = microsoft.unlockAccount.ad_unlock_account(connection, dn)
    if not success:
        raise Exception(f"Failed to unlock user {sam_account_name}")

    demisto_entry = {
        "ContentsFormat": formats["text"],
        "Type": entryTypes["note"],
        "Contents": f"Unlocked user {sam_account_name}",
    }
    demisto.results(demisto_entry)


""" DELETE OBJECT """


def delete_user():
    # can actually delete any object...
    assert connection is not None
    success = connection.delete(demisto.args().get("user-dn"))
    if not success:
        raise Exception("Failed to delete user")

    demisto_entry = {
        "ContentsFormat": formats["text"],
        "Type": entryTypes["note"],
        "Contents": "Deleted object with dn {}".format(demisto.args().get("user-dn")),
    }
    demisto.results(demisto_entry)


def delete_group():
    assert connection is not None
    args = demisto.args()

    dn = args.get("dn")

    # delete group
    success = connection.delete(dn)
    if not success:
        raise Exception("Failed to delete group")

    demisto_entry = {"ContentsFormat": formats["text"], "Type": entryTypes["note"], "Contents": f"Deleted group with DN: {dn}"}
    demisto.results(demisto_entry)


def get_mapping_fields_command(search_base):
    ad_attributes = get_all_attributes(search_base)
    # add keys that are not attributes but can be used in mapping
    ad_attributes.extend(("dn", "manageremail"))

    incident_type_scheme = SchemeTypeMapping(type_name=IAMUserProfile.DEFAULT_INCIDENT_TYPE)

    for field in ad_attributes:
        incident_type_scheme.add_field(field, "Field")

    return GetMappingFieldsResponse([incident_type_scheme])


"""
    TEST CONFIGURATION
    authenticate user credentials while initializing connection with AD server
    verify base DN is configured correctly
"""


def set_password_not_expire(default_base_dn):
    args = demisto.args()
    sam_account_name = args.get("username")
    pwd_n_exp = argToBoolean(args.get("value"))

    if not sam_account_name:
        raise Exception("Missing argument - You must specify a username (sAMAccountName).")

    # Query by sAMAccountName
    sam_account_name = escape_filter_chars(sam_account_name)
    query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={sam_account_name}))"
    entries = search_with_paging(query, default_base_dn, attributes="userAccountControl")

    if not check_if_user_exists_by_attribute(default_base_dn, "sAMAccountName", sam_account_name):
        return_error(f"sAMAccountName {sam_account_name} was not found.")

    if user := entries.get("flat"):
        user = user[0]
        if user_account_control := user.get("userAccountControl"):
            user_account_control = user_account_control[0]

        # Check if UAC flag for "Password Never Expire" (0x10000) is set to True or False:
        if pwd_n_exp:
            # Sets the bit 16 to 1
            user_account_control |= 1 << 16
            content_output = f'AD account {sam_account_name} has set "password never expire" attribute. Value is set to True'
        else:
            # Clears the bit 16 to 0
            user_account_control &= ~(1 << 16)
            content_output = f'AD account {sam_account_name} has cleared "password never expire" attribute. Value is set to False'

        attribute_name = "userAccountControl"
        attribute_value = user_account_control
        dn = user_dn(sam_account_name, default_base_dn)
        modification = {attribute_name: [("MODIFY_REPLACE", attribute_value)]}

        # Modify user
        modify_object(dn, modification)
        demisto_entry = {"ContentsFormat": formats["text"], "Type": entryTypes["note"], "Contents": content_output}
        demisto.results(demisto_entry)

    else:
        raise DemistoException(f"Unable to fetch attribute 'userAccountControl' for user {sam_account_name}.")


def test_credentials_command(server_ip, server, ntlm_connection, auto_bind):
    args = demisto.args()
    username = args.get("username")
    try:
        connection = create_connection(
            server=server,
            server_ip=server_ip,
            username=username,
            password=args.get("password"),
            ntlm_connection=argToBoolean(ntlm_connection),
            auto_bind=auto_bind,
        )
        connection.unbind()
    except LDAPBindError:
        raise DemistoException(f"Credential test with username {username} was not successful.")
    return CommandResults(
        outputs_prefix="ActiveDirectory.ValidCredentials",
        outputs_key_field="username",
        outputs=username,
        readable_output=f"Credential test with username {username} succeeded.",
    )


def create_connection(server: Server, server_ip: str, username: str, password: str, ntlm_connection: bool, auto_bind: str | bool):
    domain_name = server_ip + "\\" + username if "\\" not in username else username
    # open socket and bind to server
    return (
        Connection(server, domain_name, password=password, authentication=NTLM, auto_bind=auto_bind)
        if ntlm_connection
        else Connection(server, user=username, password=password, auto_bind=auto_bind)
    )


def get_auto_bind_value(secure_connection, unsecure) -> str:
    """
    Returns the proper auto bind value according to the desirable connection type.
    The 'TLS' in the auto_bind parameter refers to the STARTTLS LDAP operation, that can be performed only on a
    cleartext connection (unsecure connection - port 389).

    If the Client's connection type is Start TLS - the secure level will be upgraded to TLS during the
    connection bind itself, and thus we use the AUTO_BIND_TLS_BEFORE_BIND constant.

    If the Client's connection type is Start TLS and the 'Trust any certificate' is unchecked -
    For backwards compatibility - we use the AUTO_BIND_TLS_BEFORE_BIND constant as well.

    If the Client's connection type is SSL - the connection is already secured (server was initialized with
    use_ssl=True and port 636) and therefore we use the AUTO_BIND_NO_TLS constant.

    Otherwise, the Client's connection type is None - the connection is unsecured and should stay unsecured,
    thus we use the AUTO_BIND_NO_TLS constant here as well.
    """
    if secure_connection == START_TLS:
        auto_bind = AUTO_BIND_TLS_BEFORE_BIND

    elif secure_connection == TLS and not unsecure:  # BC
        auto_bind = AUTO_BIND_TLS_BEFORE_BIND

    else:
        auto_bind = AUTO_BIND_NO_TLS

    return auto_bind


def main():
    """INSTANCE CONFIGURATION"""
    params = demisto.params()
    command = demisto.command()
    args = demisto.args()

    server_ip = params.get("server_ip")
    username = params.get("credentials")["identifier"]
    password = params.get("credentials")["password"]
    default_base_dn = params.get("base_dn", "")
    secure_connection = params.get("secure_connection")
    ssl_version = params.get("ssl_version", "None")
    default_page_size = int(params.get("page_size") or 500)
    ntlm_auth = params.get("ntlm")
    insecure = params.get("unsecure", False)
    port = params.get("port")

    disabled_users_group_cn = params.get("group-cn")
    create_if_not_exists = params.get("create-if-not-exists")
    mapper_in = params.get("mapper-in", DEFAULT_INCOMING_MAPPER)
    mapper_out = params.get("mapper-out", DEFAULT_OUTGOING_MAPPER)
    verify_base_dn = params.get("verify_base_dn", True) or command == "test-module"

    if port:
        # port was configured, cast to int
        port = int(port)
    last_log_detail_level = None
    try:
        set_library_log_hide_sensitive_data(True)
        if is_debug_mode():
            demisto.info("debug-mode: setting library log detail to EXTENDED")
            last_log_detail_level = get_library_log_detail_level()
            set_library_log_detail_level(EXTENDED)

        server = initialize_server(server_ip, port, secure_connection, insecure, ssl_version)

        global connection
        auto_bind = get_auto_bind_value(secure_connection, insecure)

        try:
            # user example: domain\user
            connection = create_connection(
                server=server,
                server_ip=server_ip,
                username=username,
                password=password,
                ntlm_connection=ntlm_auth,
                auto_bind=auto_bind,
            )
        except Exception as e:
            err_msg = str(e)
            demisto.info(f"Failed connect to: {server_ip}:{port}. {type(e)}:{err_msg}\nTrace:\n{traceback.format_exc()}")
            if isinstance(e, LDAPBindError):
                message = (
                    f"Failed to bind server. Please validate that the credentials are configured correctly.\n"
                    f"Additional details: {err_msg}.\n"
                )
            elif isinstance(e, LDAPSocketOpenError | LDAPSocketReceiveError | LDAPStartTLSError):
                message = f"Failed to access LDAP server. \n Additional details: {err_msg}.\n"
                if not insecure and secure_connection in (SSL, START_TLS):
                    message += ' Try using: "Trust any certificate" option.\n'
            else:
                message = "Failed to access LDAP server. Please validate the server host and port are configured correctly.\n"
            return_error(message)
            return None

        demisto.info(f"Established connection with AD LDAP server.\nLDAP Connection Details: {connection}")

        if verify_base_dn:
            demisto.info(f'Starting to verify base DN "{default_base_dn}"')
            if not base_dn_verified(default_base_dn):
                message = (
                    f"Failed to verify the base DN configured for the instance.\n"
                    f"Last connection result: {json.dumps(connection.result)}\n"
                    f"Last error from LDAP server: {json.dumps(connection.last_error)}"
                )
                return_error(message)
                return None
            demisto.info(f'Verified base DN "{default_base_dn}"')

        """ COMMAND EXECUTION """

        if command == "test-module":
            if connection.user == "":
                # Empty response means you have no authentication status on the server, so you are an anonymous user.
                raise Exception("Failed to authenticate user")
            demisto.results("ok")

        elif command == "ad-search":
            free_search(default_base_dn, default_page_size)

        elif command == "ad-modify-password-never-expire":
            set_password_not_expire(default_base_dn)

        elif command == "ad-expire-password":
            expire_user_password(default_base_dn)

        elif command == "ad-set-new-password":
            set_user_password(default_base_dn, port)

        elif command == "ad-unlock-account":
            unlock_account(default_base_dn)

        elif command == "ad-disable-account":
            disable_user(default_base_dn, default_page_size)

        elif command == "ad-enable-account":
            enable_user(default_base_dn, default_page_size)

        elif command == "ad-remove-from-group":
            remove_member_from_group(default_base_dn)

        elif command == "ad-add-to-group":
            add_member_to_group(default_base_dn)

        elif command == "ad-create-user":
            create_user()

        elif command == "ad-delete-user":
            delete_user()

        elif command == "ad-update-user":
            update_user(default_base_dn)

        elif command == "ad-update-group":
            update_group(default_base_dn)

        elif command == "ad-modify-computer-ou":
            modify_computer_ou(default_base_dn)

        elif command == "ad-modify-user-ou":
            return_results(modify_user_ou_command(default_base_dn))

        elif command == "ad-create-contact":
            create_contact()

        elif command == "ad-update-contact":
            update_contact()

        elif command == "ad-get-user":
            search_users(default_base_dn, default_page_size)

        elif command == "ad-get-computer":
            search_computers(default_base_dn, default_page_size)

        elif command == "ad-get-group-members":
            search_group_members(default_base_dn, default_page_size)

        elif command == "ad-create-group":
            create_group()

        elif command == "ad-delete-group":
            delete_group()

        elif command == "ad-test-credentials":
            return return_results(test_credentials_command(server_ip, server, ntlm_connection=ntlm_auth, auto_bind=auto_bind))

        # IAM commands
        elif command == "iam-get-user":
            user_profile = get_user_iam(default_base_dn, args, mapper_in, mapper_out)
            return return_results(user_profile)

        elif command == "iam-create-user":
            user_profile = create_user_iam(default_base_dn, args, mapper_out, disabled_users_group_cn)
            return return_results(user_profile)

        elif command == "iam-update-user":
            user_profile = update_user_iam(default_base_dn, args, create_if_not_exists, mapper_out, disabled_users_group_cn)
            return return_results(user_profile)

        elif command == "iam-disable-user":
            user_profile = disable_user_iam(default_base_dn, disabled_users_group_cn, args, mapper_out)
            return return_results(user_profile)

        elif command == "get-mapping-fields":
            mapping_fields = get_mapping_fields_command(default_base_dn)
            return return_results(mapping_fields)

        else:
            raise NotImplementedError(f"Command {command} is not implemented")

    except Exception as e:
        message = str(e)
        if connection:
            message += (
                f"\nLast connection result: {json.dumps(connection.result)}\n"
                f"Last error from LDAP server: {connection.last_error}"
            )
        return_error(message)
        return None

    finally:
        # disconnect and close the connection
        if connection:
            connection.unbind()
        if last_log_detail_level:
            set_library_log_detail_level(last_log_detail_level)


from IAMApiModule import *  # noqa: E402

# python2 uses __builtin__ python3 uses builtins
if __name__ in ("__builtin__", "builtins", "__main__"):
    main()