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 json
import os
import socket
import ssl
import time
from threading import Thread
from unittest.mock import patch

import demistomock as demisto
import pytest
from Active_Directory_Query import group_dn, main
from IAMApiModule import *

BASE_TEST_PARAMS = {
    "server_ip": "127.0.0.1",
    "secure_connection": "None",
    "page_size": "500",
    "credentials": {"identifier": "bad", "password": "bad"},
}

RETURN_ERROR_TARGET = "Active_Directory_Query.return_error"


def test_bad_host_no_ssl(mocker):
    """
    Given:
        - Demisto.params() with an invalid server (host), and insecure connection type (None).
    When:
        - Running the 'main()' function.
    Then:
        - Verify that the expected error message was raised.
    """
    params = BASE_TEST_PARAMS
    params["server_ip"] = "127.0.0."
    mocker.patch.object(demisto, "params", return_value=BASE_TEST_PARAMS)
    return_error_mock = mocker.patch(RETURN_ERROR_TARGET)
    # validate our mock of params
    assert demisto.params().get("server_ip") == "127.0.0."
    main()
    assert return_error_mock.call_count == 1
    # call_args last call with a tuple of args list and kwargs
    err_msg = return_error_mock.call_args[0][0]
    assert len(err_msg) < 100
    assert "Failed to access" in err_msg
    assert "invalid server address" in err_msg


@pytest.mark.filterwarnings("ignore::ResourceWarning")
def test_bad_ssl(mocker):
    """
    Given:
        - Demisto.params() with an ssl connection type (SSL) and a server (host) that will cause
          an SSL socket error.
    When:
        - Running the 'main()' function.
    Then:
        - Verify that the expected error message was raised.
    """
    params = BASE_TEST_PARAMS.copy()
    params["server_ip"] = "185.199.108.153"  # disable-secrets-detection
    params["secure_connection"] = "SSL"
    params["port"] = 443
    mocker.patch.object(demisto, "params", return_value=params)
    return_error_mock = mocker.patch(RETURN_ERROR_TARGET)
    mocker.patch.object(demisto, "info")
    # validate our mock of params
    assert demisto.params().get("secure_connection") == "SSL"
    main()
    assert return_error_mock.call_count == 1
    # call_args last call with a tuple of args list and kwargs
    err_msg = return_error_mock.call_args[0][0]
    assert "Failed to access" in err_msg
    assert 'Try using: "Trust any certificate" option.' in err_msg


def ssl_bad_socket_server(port):
    context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    # cert and keyfile generated with
    # openssl req -x509 -nodes -days 3000 -newkey rsa:2048 -keyout key.pem -out cert.pem
    try:
        context.load_cert_chain("test_data/cert.pem", "test_data/key.pem")
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
            sock.bind(("127.0.0.1", port))
            sock.listen(5)
            with context.wrap_socket(sock, server_side=True) as ssock:
                try:
                    conn, addr = ssock.accept()
                except ssl.SSLError as err:
                    if "TLSV1_ALERT_UNKNOWN_CA" in str(err):
                        # all is ok. client refused our cert
                        return
                    raise
                conn.recv(32)
                msg = b"THIS IS A TEST SERVER WHICH IGNORES PROTOCOL\n\n"
                for _x in range(10):
                    msg += msg
                conn.send(msg)
                conn.shutdown(socket.SHUT_RDWR)
                conn.close()
    except Exception as ex:
        pytest.fail(f"Failed starting ssl_bad_socket_server: {ex}")
        raise


@pytest.mark.filterwarnings("ignore::ResourceWarning")
def test_faulty_server(mocker):
    port = 9638
    t = Thread(target=ssl_bad_socket_server, args=(port,))
    t.start()
    time.sleep(1)  # wait for socket server to startup
    params = BASE_TEST_PARAMS.copy()
    params["server_ip"] = "127.0.0.1"  # disable-secrets-detection
    params["secure_connection"] = "SSL"
    params["unsecure"] = True
    params["port"] = port
    mocker.patch.object(demisto, "params", return_value=params)
    return_error_mock = mocker.patch(RETURN_ERROR_TARGET)
    # validate our mock of params
    assert demisto.params().get("secure_connection") == "SSL"
    main()
    t.join(5)
    assert return_error_mock.call_count == 1
    # call_args last call with a tuple of args list and kwargs
    err_msg = return_error_mock.call_args[0][0]
    assert len(err_msg) < 125
    assert "Failed to access" in err_msg
    assert 'Try using: "Trust any certificate" option.' not in err_msg


def test_ssl_custom_cert(mocker, request):
    ENV_KEY = "SSL_CERT_FILE"
    os.environ[ENV_KEY] = "test_data/cert.pem"

    def cleanup():
        os.environ.pop(ENV_KEY)

    request.addfinalizer(cleanup)
    port = 9637
    t = Thread(target=ssl_bad_socket_server, args=(port,))
    t.start()
    time.sleep(1)  # wait for socket server to startup
    params = BASE_TEST_PARAMS.copy()
    params["server_ip"] = "127.0.0.1"  # disable-secrets-detection
    params["secure_connection"] = "SSL"
    params["port"] = port
    mocker.patch.object(demisto, "params", return_value=params)
    return_error_mock = mocker.patch(RETURN_ERROR_TARGET)
    # validate our mock of params
    assert demisto.params().get("secure_connection") == "SSL"
    main()
    t.join(5)
    assert return_error_mock.call_count == 1
    # call_args last call with a tuple of args list and kwargs
    err_msg = return_error_mock.call_args[0][0]
    assert len(err_msg) < 100
    assert "Failed to access" in err_msg


def test_endpoint_entry():
    """
    Given:
         Custom attributes to filter the computer object entry.
    When:
        The function filters the computer object according to the custom attributes.
    Then:
        The function will return all the computer object entry because custom attributes contain '*'.

    """
    from Active_Directory_Query import endpoint_entry

    custom_attributes_with_asterisk = endpoint_entry({"dn": "dn", "name": "name", "memberOf": "memberOf"}, ["*"])
    assert custom_attributes_with_asterisk == {"Groups": "memberOf", "Hostname": "name", "ID": "dn", "Type": "AD"}


def get_outputs_from_user_profile(user_profile):
    entry_context = user_profile.to_entry()
    outputs = entry_context.get("Contents")

    return outputs


def mock_demisto_map_object(object, mapper_name, incident_type):
    email = object.get("email")
    email_prefix = email.split("@")[0]
    return {
        "cn": email_prefix,
        "mail": email,
        "sAMAccountName": email_prefix,
        "userPrincipalName": email_prefix,
        "ou": "OU=Americas,OU=Demisto",
    }


def test_get_iam_user_profile(mocker):
    from Active_Directory_Query import get_iam_user_profile

    mocker.patch.object(demisto, "mapObject", side_effect=mock_demisto_map_object)

    user_profile = {
        "email": "test2@paloaltonetworks.com",
        "username": "test",
        "locationregion": "Americas",
        "olduserdata": {"email": "test@paloaltonetworks.com", "username": "test", "locationregion": "Americas"},
    }
    _, ad_user, sam_account_name = get_iam_user_profile(user_profile, "mock_mapper_out")
    assert sam_account_name == "test"
    assert ad_user


def test_update_user_iam__username_change(mocker):
    """
    Given:
         A valid user profile with valid mapping
    When:
        Running the `create_user_iam` command
    Then:
        The user was created successfully in AD.

    """
    import Active_Directory_Query

    add_args, add_kwargs = [], {}

    class ConnectionMocker:
        entries = []
        result = {"controls": {"1.2.840.113556.1.4.319": {"value": {"cookie": "<cookie>"}}}}

        def search(self, *args, **kwargs):
            return

        def add(self, *args, **kwargs):
            nonlocal add_args, add_kwargs
            add_args, add_kwargs = args, kwargs
            return True

        def modify(self, *args, **kwargs):
            return True

        def modify_dn(self, *args, **kwargs):
            return True

    Active_Directory_Query.connection = ConnectionMocker()
    args = {
        "user-profile": json.dumps(
            {
                "email": "test2@paloaltonetworks.com",
                "username": "test",
                "locationregion": "Americas",
                "olduserdata": {"email": "test@paloaltonetworks.com", "username": "test", "locationregion": "Americas"},
            }
        )
    }

    mocker.patch.object(demisto, "mapObject", side_effect=mock_demisto_map_object)
    mocker.patch("Active_Directory_Query.check_if_user_exists_by_attribute", return_value=True)
    mocker.patch("Active_Directory_Query.get_user_activity_by_samaccountname", return_value=True)
    mocker.patch("Active_Directory_Query.user_dn", return_value="mock_dn")

    user_profile = Active_Directory_Query.update_user_iam(
        default_base_dn="mock_base_dn",
        args=args,
        create_if_not_exists=False,
        mapper_out="mock_mapper_out",
        disabled_users_group_cn="mock_disabled_users_group_cn",
    )
    outputs = get_outputs_from_user_profile(user_profile)
    assert outputs.get("action") == IAMActions.UPDATE_USER
    assert outputs.get("success") is True
    assert outputs.get("email") == "test2@paloaltonetworks.com"
    assert outputs.get("username") == "test2"


def test_create_user_iam(mocker):
    """
    Given:
         A valid user profile with valid mapping
    When:
        Running the `create_user_iam` command
    Then:
        The user was created successfully in AD.

    """
    import Active_Directory_Query

    add_args, add_kwargs = [], {}

    class ConnectionMocker:
        entries = []
        result = {"controls": {"1.2.840.113556.1.4.319": {"value": {"cookie": "<cookie>"}}}}

        def search(self, *args, **kwargs):
            return

        def add(self, *args, **kwargs):
            nonlocal add_args, add_kwargs
            add_args, add_kwargs = args, kwargs
            return True

    Active_Directory_Query.connection = ConnectionMocker()
    args = {"user-profile": json.dumps({"email": "test@paloaltonetworks.com", "username": "test", "locationregion": "Americas"})}

    mocker.patch("Active_Directory_Query.check_if_user_exists_by_attribute", return_value=False)
    mocker.patch.object(
        IAMUserProfile,
        "map_object",
        return_value={
            "cn": "test",
            "mail": "test@paloaltonetworks.com",
            "sAMAccountName": "test",
            "userPrincipalName": "test",
            "ou": "OU=Americas,OU=Demisto",
        },
    )

    user_profile = Active_Directory_Query.create_user_iam("", args, "mapper_out", "")
    outputs = get_outputs_from_user_profile(user_profile)
    assert outputs.get("action") == IAMActions.CREATE_USER
    assert outputs.get("success") is True
    assert outputs.get("active") is False
    assert outputs.get("email") == "test@paloaltonetworks.com"


def test_unseccsseful_create_user_iam_missing_ou(mocker):
    """
    Given:
         A valid user profile with missing ou in the mapping
    When:
        Running the `create_user_iam` command
    Then:
        - The user was not created in AD.
        - An error message was returned.

    """
    import Active_Directory_Query

    add_args, add_kwargs = [], {}

    class ConnectionMocker:
        entries = []
        result = {"controls": {"1.2.840.113556.1.4.319": {"value": {"cookie": "<cookie>"}}}}

        def search(self, *args, **kwargs):
            return

        def add(self, *args, **kwargs):
            nonlocal add_args, add_kwargs
            add_args, add_kwargs = args, kwargs
            return True

    Active_Directory_Query.connection = ConnectionMocker()
    args = {"user-profile": json.dumps({"email": "test@paloaltonetworks.com", "username": "test", "locationregion": "Americas"})}

    mocker.patch("Active_Directory_Query.check_if_user_exists_by_attribute", return_value=False)
    mocker.patch.object(
        IAMUserProfile,
        "map_object",
        return_value={"cn": "test", "mail": "test@paloaltonetworks.com", "sAMAccountName": "test", "userPrincipalName": "test"},
    )

    user_profile = Active_Directory_Query.create_user_iam("", args, "mapper_out", "")
    outputs = get_outputs_from_user_profile(user_profile)
    assert outputs.get("action") == IAMActions.CREATE_USER
    assert outputs.get("success") is False
    assert outputs.get("email") == "test@paloaltonetworks.com"
    assert "User must have an Organizational Unit (OU)" in outputs.get("errorMessage")


def test_unseccsseful_create_user_iam_missing_samaccountname(mocker):
    """
    Given:
         A valid user profile with missing samaccountname in the mapping
    When:
        Running the `create_user_iam` command
    Then:
        - The user was not created in AD.
        - An error message was returned.

    """
    import Active_Directory_Query

    add_args, add_kwargs = [], {}

    class ConnectionMocker:
        entries = []
        result = {"controls": {"1.2.840.113556.1.4.319": {"value": {"cookie": "<cookie>"}}}}

        def search(self, *args, **kwargs):
            return

        def add(self, *args, **kwargs):
            nonlocal add_args, add_kwargs
            add_args, add_kwargs = args, kwargs
            return True

    Active_Directory_Query.connection = ConnectionMocker()
    args = {"user-profile": json.dumps({"email": "test@paloaltonetworks.com", "username": "test", "locationregion": "Americas"})}

    mocker.patch("Active_Directory_Query.check_if_user_exists_by_attribute", return_value=False)
    mocker.patch.object(
        IAMUserProfile,
        "map_object",
        return_value={
            "cn": "test",
            "mail": "test@paloaltonetworks.com",
            "ou": "OU=Americas,OU=Demisto",
            "userPrincipalName": "test",
        },
    )

    user_profile = Active_Directory_Query.create_user_iam("", args, "mapper_out", "")
    outputs = get_outputs_from_user_profile(user_profile)
    assert outputs.get("action") == IAMActions.CREATE_USER
    assert outputs.get("success") is False
    assert outputs.get("email") == "test@paloaltonetworks.com"
    assert "User must have a sAMAccountName" in outputs.get("errorMessage")


def test_group_entry_no_custom_attributes():
    """
    Given:
         Custom attributes to filter the group object entry.
    When:
        The function filters the group object according to the custom attributes.
    Then:
        The function will return all the group object entry because custom attributes contain '*'.

    """
    from Active_Directory_Query import group_entry

    custom_attributes_with_asterisk = group_entry({"dn": "dn", "name": "name", "memberOf": "memberOf"}, ["*"])
    assert custom_attributes_with_asterisk == {"Groups": "memberOf", "ID": "dn", "Name": "name", "Type": "AD"}


def test_group_entry():
    """
    Given:
         Custom attributes to filter the group object entry.
    When:
        The function filters the group object according to the custom attributes.
    Then:
        The function will return all the group object entry because custom attributes contain '*'.

    """
    from Active_Directory_Query import group_entry

    custom_attributes_with_asterisk = group_entry(
        {"dn": "dn", "name": "name", "memberOf": "memberOf", "displayName": "display name"}, ["displayName"]
    )
    assert custom_attributes_with_asterisk == {
        "Groups": "memberOf",
        "ID": "dn",
        "Name": "name",
        "Type": "AD",
        "displayName": "display name",
    }


def test_search_group_members(mocker):
    """
    sanity test for search_group_members method
    """
    import Active_Directory_Query

    class EntryMocker:
        def entry_to_json(self):
            return '{"dn": "dn","attributes": {"memberOf": ["memberOf"], "name": ["name"]}}'

    class ConnectionMocker:
        entries = [EntryMocker()]
        result = {"controls": {"1.2.840.113556.1.4.319": {"value": {"cookie": b"<cookie>"}}}}

        def search(self, *args, **kwargs):
            time.sleep(1)

    expected_entry = {
        "ActiveDirectory.Groups(obj.dn ==dn)": {"dn": "dn", "members": [{"dn": "dn", "category": "group"}]},
        "ActiveDirectory.Groups(obj.dn == val.dn)": [{"dn": "dn", "memberOf": ["memberOf"], "name": ["name"]}],
        "Group": [{"Type": "AD", "ID": "dn", "Name": ["name"], "Groups": ["memberOf"]}],
        "ActiveDirectory(true)": {"GroupsPageCookie": base64.b64encode(b"<cookie>").decode("utf-8")},
    }

    expected_results = {
        "ContentsFormat": "json",
        "Type": 1,
        "Contents": [{"dn": "dn", "attributes": {"memberOf": ["memberOf"], "name": ["name"]}}],
        "ReadableContentsFormat": "markdown",
        "HumanReadable": "### Active Directory - Get Group Members\n|"
        "dn|memberOf|name|\n|---|---|---|\n| dn | memberOf | name |\n",
        "EntryContext": expected_entry,
    }
    expected_results = f"demisto results: {json.dumps(expected_results, indent=4, sort_keys=True)}"

    mocker.patch.object(demisto, "args", return_value={"member-type": "group", "group-dn": "dn", "time_limit": "1"})

    Active_Directory_Query.connection = ConnectionMocker()

    with patch("logging.Logger.info") as mock:
        Active_Directory_Query.search_group_members("dc", 1)
        mock.assert_called_with(expected_results)


def test_group_dn_escape_characters():
    """
    Given:
         Group name with parentheses
    When:
        Running the function group_dn
    Then:
        The function search gets the group name after escape special characters.

    """
    import Active_Directory_Query

    class EntryMocker:
        def entry_to_json(self):
            return '{"dn": "dn","attributes": {"memberOf": ["memberOf"], "name": ["name"]}}'

    class ConnectionMocker:
        entries = [EntryMocker()]
        result = {"controls": {"1.2.840.113556.1.4.319": {"value": {"cookie": "<cookie>"}}}}

    Active_Directory_Query.connection = ConnectionMocker()

    with patch("Active_Directory_Query.search", return_value=[EntryMocker()]) as mock:
        group_dn("group(group)", "")

        mock.assert_called_with("(&(objectClass=group)(cn=group\\28group\\29))", "")


def test_search__no_control_exist(mocker):
    """
    Given:
         No control key in the result
    When:
        Run any search query
    Then:
        The result return 'no entries' instead of throw exception

    """
    import Active_Directory_Query

    class ConnectionMocker:
        entries = []
        result = {}

        def search(self, *args, **kwargs):
            return

    mocker.patch.object(demisto, "results")
    Active_Directory_Query.connection = ConnectionMocker()
    Active_Directory_Query.search_users("dc=test,dc=test_1", page_size=20)

    assert "**No entries.**" in demisto.results.call_args[0][0]["HumanReadable"]


def test_search_attributes_to_exclude(mocker):
    """
    Given:
        attributes_to_exclude
    When:
        Run any search query
    Then:
        The given arguments where excluded from human_readable and context_data
    """
    import Active_Directory_Query

    class EntryMocker:
        def entry_to_json(self):
            return '{"dn": "dn"}'

    class ConnectionMocker:
        entries = [EntryMocker()]
        result = {"controls": {"1.2.840.113556.1.4.319": {"value": {"cookie": b"<cookie>"}}}}

        def search(self, *args, **kwargs):
            time.sleep(1)

    expected_results = {
        "ContentsFormat": "json",
        "Type": 1,
        "Contents": [{"dn": "dn"}],
        "ReadableContentsFormat": "markdown",
        "HumanReadable": "### Active Directory - Get Users\n|dn|\n|---|\n| dn |\n",
        "EntryContext": {
            "ActiveDirectory.Users(obj.dn == val.dn)": [{"dn": "dn"}],
            "Account(obj.ID == val.ID)": [
                {
                    "Type": "AD",
                    "ID": "dn",
                    "Email": None,
                    "Username": None,
                    "DisplayName": None,
                    "Managr": None,
                    "Manager": None,
                    "Groups": None,
                }
            ],
            "ActiveDirectory(true)": {"UsersPageCookie": base64.b64encode(b"<cookie>").decode("utf-8")},
        },
    }

    expected_results = f"demisto results: {json.dumps(expected_results, indent=4, sort_keys=True)}"

    mocker.patch.object(
        demisto,
        "args",
        return_value={
            "attributes-to-exclude": "memberOf,name,mail,displayName,manager,sAMAccountName,userAccountControl",
            "page-size": "1",
        },
    )

    Active_Directory_Query.connection = ConnectionMocker()

    with patch("logging.Logger.info") as mock:
        Active_Directory_Query.search_users("dc", 1)
        mock.assert_called_with(expected_results)


def test_user_account_to_boolean_fields():
    """
    Given:
        a userAccountControl value
    When:
        parsing the userAccountControl fields
    Then:
        Only the relevant fields will be marked as true
    """
    import Active_Directory_Query

    fields = Active_Directory_Query.user_account_to_boolean_fields(0x50)
    assert {k for k, v in fields.items() if v} == {"LOCKOUT", "PASSWD_CANT_CHANGE"}


@pytest.mark.parametrize("flags", [512, 0, 544])
def test_restore_user(mocker, flags):
    """
    Given:
        A disabled user.
    When:
        Calling restore_user method.
    Then:
        Verify the existing flag is returned.
    """
    from Active_Directory_Query import restore_user

    re_val = {"flat": [{"userAccountControl": [flags]}]}
    mocker.patch("Active_Directory_Query.search_with_paging", return_value=re_val)
    mocker.patch.object(demisto, "args")

    assert restore_user("test_user", 0) == flags


def test_enable_user_with_restore_user_option(mocker):
    """
    Given:
        A disabled user.
    When:
        Calling enable_user method.
    Then:
        Verify the existing flag is returned with the disable bit off.
    """
    from Active_Directory_Query import enable_user

    disabled_account_with_properties = 546
    enabled_account_with_properties = 544
    mocker.patch("Active_Directory_Query.restore_user", return_value=disabled_account_with_properties)
    mocker.patch("Active_Directory_Query.user_dn", return_value="test_dn")
    modify_data = mocker.patch("Active_Directory_Query.modify_object")
    mocker.patch.object(demisto, "args")

    enable_user("test_user", 0)

    assert modify_data.call_args.args[1].get("userAccountControl")[0][1] == enabled_account_with_properties


def test_search_with_paging_bug(mocker):
    """
     Given:
        page size larger than 1.
    When:
        running get-group-members command.
    Then:
        time_limit results returned.

    """
    import Active_Directory_Query

    class EntryMocker:
        def entry_to_json(self):
            return '{"dn": "dn","attributes": {"memberOf": ["memberOf"], "name": ["name"]}}'

    class ConnectionMocker:
        entries = []
        result = {"controls": {"1.2.840.113556.1.4.319": {"value": {"cookie": b"<cookie>"}}}}

        def search(self, *args, **kwargs):
            page_size = kwargs.get("paged_size")
            if page_size:
                self.entries = [EntryMocker() for i in range(page_size)]
                time.sleep(1)

    mocker.patch.object(demisto, "results")
    mocker.patch.object(demisto, "args", return_value={"member-type": "group", "group-dn": "dn", "time_limit": "3"})

    Active_Directory_Query.connection = ConnectionMocker()

    with patch("logging.Logger.info"):
        Active_Directory_Query.search_group_members("dc", 1)
        assert len(demisto.results.call_args[0][0]["Contents"]) == 3


def test_password_not_expire_missing_username(mocker):
    """
     Given:
        A demisto args object with missing username and a valid value.
    When:
        running set_password_not_expire command.
    Then:
        Verify that a a missing username exception is raised.

    """
    from Active_Directory_Query import set_password_not_expire

    mocker.patch.object(demisto, "args", return_value={"username": None, "value": True})
    default_base_dn = {}

    with pytest.raises(Exception) as err:
        set_password_not_expire(default_base_dn)
    assert err.value.args[0] == "Missing argument - You must specify a username (sAMAccountName)."


@pytest.mark.parametrize(
    "connection_type, unsecure, expected_auto_bind_value",
    [
        ("Start TLS", True, "TLS_BEFORE_BIND"),
        ("Start TLS", False, "TLS_BEFORE_BIND"),
        ("TLS", False, "TLS_BEFORE_BIND"),
        ("TLS", True, "NO_TLS"),
        ("SSL", True, "NO_TLS"),
        ("SSL", False, "NO_TLS"),
        ("None", True, "NO_TLS"),
        ("None", False, "NO_TLS"),
    ],
)
def test_get_auto_bind_value(connection_type, unsecure, expected_auto_bind_value):
    """
    Given:
        - A connection type:
            1. Start TLS
            2. TLS
            3. SSL
            4. None
    When:
        - Running the 'get_auto_bind_value()' function.
    Then:
        - Verify that the returned auto_bind value is as expected:
            1. 'TLS_BEFORE_BIND' - which means that connection should upgrade it's secure level to TLS before
                                   the bind itself (STARTTLS command is executed).

            2. 'TLS_BEFORE_BIND' - for unsecure=False and 'NO_TLS' for unsecure=True

            3. 'NO_TLS' - The connection is secured from the beginning,
                          thus STARTTLS command shouldn't be executed.

            4. 'NO_TLS' - Connection is insecure (cleartext) and shouldn't be upgraded to TLS.
    """
    from Active_Directory_Query import get_auto_bind_value

    auto_bind_value = get_auto_bind_value(connection_type, unsecure)
    assert auto_bind_value == expected_auto_bind_value


@pytest.mark.parametrize(
    "ssl_version, expected_ssl_version",
    [("TLS", 2), ("TLSv1", 3), ("TLSv1_1", 4), ("TLSv1_2", 5), ("TLS_CLIENT", 16), (None, None), ("None", None)],
)
def test_get_ssl_version(ssl_version, expected_ssl_version):
    """
    Given:
        - An ssl protocol version:
            1. TLS
            2. TLSv1
            3. TLSv1_1
            4. TLSv1_2
            5. TLS_CLIENT
            6. None
            7. 'None'
    When:
        - Running the 'get_ssl_version()' function.
    Then:
        - Verify that the returned ssl version value is as expected:
            1. TLS - 2
            2. TLSv1 - 3
            3. TLSv1_1 - 4
            4. TLSv1_2 - 5
            5. TLS_CLIENT - 16
            6. None - None
            7. 'None' - None
    """
    from Active_Directory_Query import get_ssl_version

    ssl_version_value = get_ssl_version(ssl_version)
    assert ssl_version_value == expected_ssl_version


def test_search_users_empty_userAccountControl(mocker):
    """
    Given:
        The 'userAccountControl' attribute was returned empty
    When:
        Run the 'ad-get-user' command
    Then:
        The result returns without raise IndexError: list index out of range
    """

    import Active_Directory_Query

    class EntryMocker:
        def entry_to_json(self):
            return (
                '{"attributes": {"displayName": [], "mail": [], "manager": [], "memberOf": ["memberOf"], '
                '"name": ["Guest"], "sAMAccountName": ["Guest"], "userAccountControl": []}, "dn": "test_dn"}'
            )

    class ConnectionMocker:
        entries = [EntryMocker()]
        result = {"controls": {"1.2.840.113556.1.4.319": {"value": {"cookie": b"<cookie>"}}}}

        def search(self, *args, **kwargs):
            time.sleep(1)

    expected_results = {
        "ContentsFormat": "json",
        "Type": 1,
        "Contents": [
            {
                "attributes": {
                    "displayName": [],
                    "mail": [],
                    "manager": [],
                    "memberOf": ["memberOf"],
                    "name": ["Guest"],
                    "sAMAccountName": ["Guest"],
                    "userAccountControl": [],
                },
                "dn": "test_dn",
            }
        ],
        "ReadableContentsFormat": "markdown",
        "HumanReadable": "### Active Directory - Get Users\n|displayName|dn|mail|manager|memberOf|name"
        "|sAMAccountName|userAccountControl|\n|---|---|---|---|---|---|---|---|\n|  |"
        " test_dn |  |  | memberOf | Guest | Guest |  |\n",
        "EntryContext": {
            "ActiveDirectory.Users(obj.dn == val.dn)": [
                {
                    "dn": "test_dn",
                    "displayName": [],
                    "mail": [],
                    "manager": [],
                    "memberOf": ["memberOf"],
                    "name": ["Guest"],
                    "sAMAccountName": ["Guest"],
                    "userAccountControl": [],
                }
            ],
            "Account(obj.ID == val.ID)": [
                {
                    "Type": "AD",
                    "ID": "test_dn",
                    "Email": [],
                    "Username": ["Guest"],
                    "DisplayName": [],
                    "Managr": [],
                    "Manager": [],
                    "Groups": ["memberOf"],
                }
            ],
            "ActiveDirectory(true)": {"UsersPageCookie": base64.b64encode(b"<cookie>").decode("utf-8")},
        },
    }

    expected_results = f"demisto results: {json.dumps(expected_results, indent=4, sort_keys=True)}"

    mocker.patch.object(demisto, "args", return_value={"page-size": "1"})

    Active_Directory_Query.connection = ConnectionMocker()

    with patch("logging.Logger.info") as mock:
        Active_Directory_Query.search_users("dc", 1)
        mock.assert_called_with(expected_results)


def test_test_credentials_command(mocker):
    """
    Given:
        A demisto args object with username and password
    When:
        Run the 'ad-test-credentials' command
    Then:
        The result returns with successful connection
    """
    import Active_Directory_Query

    args = {"username": "username_test_credentials", "password": "password_test_credentials"}
    mocker.patch.object(demisto, "args", return_value=args)

    class MockConnection:
        def unbind(self):
            pass

    def mock_create_connection(server, server_ip, username, password, ntlm_connection, auto_bind):
        return MockConnection()

    with (
        patch("Active_Directory_Query.create_connection", side_effect=mock_create_connection),
        patch("Active_Directory_Query.Connection.unbind", side_effect=MockConnection.unbind),
    ):
        command_results = Active_Directory_Query.test_credentials_command(
            BASE_TEST_PARAMS["server_ip"], "server", ntlm_connection="true", auto_bind="auto_bind"
        )
        assert command_results.readable_output == "Credential test with username username_test_credentials succeeded."


@pytest.mark.parametrize(
    "dn,expected",
    [
        ("CN=name, lastname,OU=Test1,DC=dc1,DC=dc2", "CN=name, lastname"),
        ("CN=name\\ lastname,OU=Test1,DC=dc1,DC=dc2", "CN=name lastname"),
        ("CN=name,DC=dc1,DC=dc2", "CN=name"),
    ],
)
def test_modify_user_ou(mocker, dn, expected):
    """
    Given:
         - user with CN contains //
         - user with CN contains comma
         - user without ou
    When:
        Run the 'ad-modify-ou' command
    Then:
         Validate the cn extracted as expected
    """
    import Active_Directory_Query

    class MockConnection:
        def modify_dn(self, dn, cn, new_superior):
            pass

    Active_Directory_Query.connection = MockConnection()
    new_ou = "OU=Test2"
    connection_mocker = mocker.patch.object(Active_Directory_Query.connection, "modify_dn", return_value=True)
    Active_Directory_Query.modify_user_ou(dn, new_ou)
    assert connection_mocker.call_args[0][1] == expected


def test_search_users_with_msDSUserAccountControlComputed(mocker):
    """
    Given:
        The 'msDSUserAccountControlComputed' was returned.
    When:
        Run the 'ad-get-user' command
    Then:
        The user_account_to_boolean_fields_msDS_user_account_control_computed was called.
    """

    import Active_Directory_Query

    class EntryMocker:
        def entry_to_json(self):
            return (
                '{"attributes": {"displayName": [], "mail": [], "manager": [], "memberOf": ["memberOf"], '
                '"name": ["Guest"], "sAMAccountName": ["Guest"], "userAccountControl": [0], \
                   "msDS-User-Account-Control-Computed": [0]},"dn": "test_dn"}'
            )

    class ConnectionMocker:
        entries = [EntryMocker()]
        result = {"controls": {"": {"value": {"cookie": b"<cookie>"}}}}

        def search(self, *args, **kwargs):
            time.sleep(1)

    mocker.patch.object(demisto, "args", return_value={"page-size": "1"})
    mocker.patch.object(demisto, "results")
    mocker_msDSUserAccountControlComputed = mocker.patch.object(
        Active_Directory_Query,
        "user_account_to_boolean_fields_msDS_user_account_control_computed",
        return_value={"PASSWORD_EXPIRED": True, "LOCKOUT": True},
    )

    Active_Directory_Query.connection = ConnectionMocker()

    Active_Directory_Query.search_users("dc", 1)
    mocker_msDSUserAccountControlComputed.assert_called_once()
    assert "msDS-User-Account-Control-Computed" in demisto.results.call_args[0][0]["Contents"][0].get("attributes")
    assert (
        demisto.results.call_args[0][0]["EntryContext"]
        .get("ActiveDirectory.Users(obj.dn == val.dn)", {})[0]
        .get("userAccountControlFields")
        .get("PASSWORD_EXPIRED")
        is True
    )


def test_base_dn_verified_success(mocker):
    """
    Given:
        A valid base DN and a successful LDAP connection.
    When:
        Running the 'base_dn_verified' function.
    Then:
        Verify that the function returns True and uses BASE scope with optimized parameters.
    """
    import Active_Directory_Query
    from ldap3 import BASE

    search_args = []
    search_kwargs = {}

    class ConnectionMocker:
        result = {"description": "success"}

        def search(self, *args, **kwargs):
            nonlocal search_args, search_kwargs
            search_args = args
            search_kwargs = kwargs
            return True

    Active_Directory_Query.connection = ConnectionMocker()
    mocker.patch.object(demisto, "info")

    result = Active_Directory_Query.base_dn_verified("dc=example,dc=com")

    # Verify the function returns True
    assert result is True

    # Verify the search was called with optimized parameters
    assert search_kwargs.get("search_base") == "dc=example,dc=com"
    assert search_kwargs.get("search_filter") == "(objectClass=*)"
    assert search_kwargs.get("search_scope") == BASE  # Verify BASE scope is used
    assert search_kwargs.get("size_limit") == 1  # Verify size limit is 1
    assert search_kwargs.get("attributes") == ["1.1"]  # Verify no attributes are fetched


def test_base_dn_verified_failure(mocker):
    """
    Given:
        An invalid base DN that causes the LDAP search to fail.
    When:
        Running the 'base_dn_verified' function.
    Then:
        Verify that the function returns False and logs the failure.
    """
    import Active_Directory_Query

    class ConnectionMocker:
        result = {"description": "noSuchObject"}

        def search(self, *args, **kwargs):
            return False

    Active_Directory_Query.connection = ConnectionMocker()
    info_mock = mocker.patch.object(demisto, "info")

    result = Active_Directory_Query.base_dn_verified("dc=invalid,dc=com")

    # Verify the function returns False
    assert result is False

    # Verify that the failure was logged
    assert info_mock.call_count == 1
    assert "Base DN verification failed" in info_mock.call_args[0][0]


def test_base_dn_verified_exception(mocker):
    """
    Given:
        A base DN that causes an exception during LDAP search.
    When:
        Running the 'base_dn_verified' function.
    Then:
        Verify that the function returns False and logs the error.
    """
    import Active_Directory_Query

    class ConnectionMocker:
        def search(self, *args, **kwargs):
            raise Exception("Connection timeout")

    Active_Directory_Query.connection = ConnectionMocker()
    error_mock = mocker.patch.object(demisto, "error")

    result = Active_Directory_Query.base_dn_verified("dc=example,dc=com")

    # Verify the function returns False
    assert result is False

    # Verify that the error was logged
    assert error_mock.call_count == 1
    assert "Error during Base DN verification" in error_mock.call_args[0][0]
    assert "Connection timeout" in error_mock.call_args[0][0]


# ===== Tests for prepare_attribute_value =====


@pytest.mark.parametrize(
    "attribute_value, attribute_type, expected",
    [
        pytest.param("displayName", None, "displayName", id="no-type-returns-string"),
        pytest.param("some value", "", "some value", id="empty-type-returns-string"),
        pytest.param(
            "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0",
            "byte",
            bytes(21),
            id="byte-type-21-zeros-logonHours-full-lockout",
        ),
        pytest.param("0, 255, 128", "byte", bytes([0, 255, 128]), id="byte-type-with-whitespace"),
        pytest.param("1,2,3", "BYTE", bytes([1, 2, 3]), id="byte-type-case-insensitive"),
    ],
)
def test_prepare_attribute_value_success(attribute_value, attribute_type, expected):
    """
    Given:
        A valid attribute value and an optional attribute type.
    When:
        Calling prepare_attribute_value.
    Then:
        The correct Python object is returned (bytes for 'byte' type, str otherwise).
    """
    from Active_Directory_Query import prepare_attribute_value

    result = prepare_attribute_value(attribute_value, attribute_type)
    assert result == expected


@pytest.mark.parametrize(
    "attribute_value, attribute_type, expected_error_fragment",
    [
        pytest.param(
            "[byte[]](0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)",
            "byte",
            "cannot be parsed as a comma-separated list of integers",
            id="byte-type-powershell-syntax-rejected",
        ),
        pytest.param("0,256,0", "byte", "out-of-range integers", id="byte-type-value-above-255"),
        pytest.param("0,-1,0", "byte", "out-of-range integers", id="byte-type-negative-value"),
        pytest.param(None, "byte", "attribute-value must be provided", id="byte-type-none-value"),
        pytest.param("", "byte", "attribute-value must be provided", id="byte-type-empty-string"),
    ],
)
def test_prepare_attribute_value_errors(attribute_value, attribute_type, expected_error_fragment):
    """
    Given:
        An invalid attribute value for the given attribute type.
    When:
        Calling prepare_attribute_value.
    Then:
        A DemistoException is raised with a descriptive message.
    """
    from CommonServerPython import DemistoException

    from Active_Directory_Query import prepare_attribute_value

    with pytest.raises(DemistoException, match=expected_error_fragment):
        prepare_attribute_value(attribute_value, attribute_type)


def test_update_user_with_byte_attribute(mocker):
    """
    Given:
        The ad-update-user command is called with attribute-type='byte' and a comma-separated
        list of integers representing logonHours (21 zero bytes = full lockout).
    When:
        update_user() is executed.
    Then:
        The LDAP modify call receives a raw bytes object (not a string) as the attribute value,
        which is what the AD server requires for Octet String attributes.
    """
    import Active_Directory_Query

    class MockConnection:
        result = {"description": "success", "result": 0}
        last_dn: str = ""
        last_modification: dict = {}

        def modify(self, dn, modification):
            self.last_dn = dn
            self.last_modification = modification
            self.result = {"description": "success", "result": 0}
            return True

    mock_conn = MockConnection()
    Active_Directory_Query.connection = mock_conn

    # Mock user_dn so we skip the LDAP search entirely
    mocker.patch.object(
        Active_Directory_Query,
        "user_dn",
        return_value="CN=Test User,DC=example,DC=com",
    )
    mocker.patch.object(
        demisto,
        "args",
        return_value={
            "username": "testuser",
            "attribute-name": "logonHours",
            "attribute-value": "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0",
            "attribute-type": "byte",
        },
    )
    mocker.patch.object(demisto, "results")

    Active_Directory_Query.update_user("dc=example,dc=com")

    # The value sent to LDAP must be raw bytes, not a string
    sent_value = mock_conn.last_modification["logonHours"][0][1]
    assert isinstance(sent_value, bytes), f"Expected bytes, got {type(sent_value)}"
    assert sent_value == bytes(21)