Vectra_Detect

This integration allows to create incidents based on Vectra Accounts/Hosts/Detections objects.

Network Security · Vectra AI

Details

IDVectra_Detect
ProviderVectra AI
CategoryNetwork Security
From Version6.5.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

This integration allows to create incidents based on Vectra Accounts/Hosts/Detections objects.
This integration was integrated and tested with version 7.1 of Vectra Detect

Use cases

  1. Fetch accounts, hosts and detections from Vectra Detect.
  2. Bi-Directional mirroring for accounts and hosts.
  3. List and describe accounts, hosts, detections, and users.
  4. List, describe, create, and resolve assignments for accounts and hosts.
  5. List, describe, and create assignment outcomes.
  6. List, create, update, and delete notes for accounts, hosts, and detections.
  7. List, create, and remove tags for accounts, hosts, and detections.
  8. List, assign, and unassign members in group.
  9. Mark and unmark detection as fixed.
  10. Mark all detections as fixed for accounts and hosts.
  11. Get detection’s PCAP file.
  12. Clean up all incidents in Cortex XSOAR by closing duplicate incidents from Vectra Detect.

Configure Vectra Detect on Cortex XSOAR

  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for Vectra Detect.
  3. Click Add instance to create and configure a new integration instance.

    Parameter Description Required
    Vectra Detect FQDN or IP Enter the FQDN or IP to reach the Vectra Detect API. (e.g. “my-vectra-box.local” or “192.168.1.1”) True
    Authentication Type Select the authentication type (Default - API Token). False
    API Token Enter the API token that can be retrieved from the Vectra UI > My Profile > General (tab) > API Token. You can also use the XSOAR credentials wallet to store it. In that case, the token should be the password. False
    Client ID and Client Secret Key Enter the Client ID and Client Secret Key that can be retrieved from the Vectra UI > Manage > API Clients. You can also use the XSOAR credentials wallet to store it. False
    Trust any certificate (not secure) When checked, no SSL certificates check will be done when interacting with the Vectra Detect API. It’s insecure. (Default - unchecked) False
    Use system proxy settings Use the system proxy settings to reach with the Vectra Detect API. False
    Fetch incidents   False
    Incident type   False
    First fetch timestamp The date or relative timestamp from which to begin fetching entities.

    Supported formats: 2 minutes, 2 hours, 2 days, 2 weeks, 2 months, 2 years, yyyy-mm-dd, yyyy-mm-ddTHH:MM:SSZ.

    For example: 01 May 2024, 01 Aug 2024 04:45:33, 2024-07-17T14:05:44Z. (default - 7 days)
    False
    Mirroring Direction The mirroring direction in which to mirror the account and host. You can mirror “Incoming” (from Vectra to Cortex XSOAR), “Outgoing” (from Cortex XSOAR to Vectra), or in both directions. False
    Re-Fetch closed incidents via mirroring If selected, new incidents will be created (via Outgoing Mirroring). If not selected, it reopens previously closed incidents (via Incoming Mirroring).

    Note: This flow is triggered only when the relevant account or host is still active and the previously fetched incident is closed.
    False
    Mirror tag for notes The tag value should be used to mirror the account and host note by adding the same tag in the notes. False
    Entity types to fetch Choose what to fetch - Accounts and/or Hosts and/or Detections. (Default - Accounts,Hosts) False
    Tags Only Accounts or Hosts that contain any of the tags specified will be fetched.

    Note: For the partial match of the tag, use ‘*’ at the start and end of word (Only a single word is allowed). Ex. *MDR*.
    False
    Detection Category Filter the detections belonging to a specified category displayed as part of layout.

    Note: This filter applies on the ‘Vectra Account’ and ‘Vectra Host’ incident type.
    False
    Detection Type Filter the detections belonging to a specified type displayed as part of layout.

    Note: This filter applies on the ‘Vectra Account’ and ‘Vectra Host’ incident type.
     
    Hosts fetch query Only “active” Hosts matching this fetch query will be fetched. Will be used only if “Hosts” is selected in the “Entity types to fetch”. (default - host.threat:>=50) False
    Accounts fetch query Only “active” Accounts matching this fetch query will be fetched. Will be used only if “Accounts” is selected in the “Entity types to fetch”. (default - account.threat:>=50) False
    Detections fetch query Only “active” Detections matching this fetch query will be fetched. Will be used only if “Detections” is selected in the “Entity types to fetch”. (default - detection.threat:>=50 AND detection.certainty:>=50) False
    Max created incidents per fetch The maximum number of new incidents to create per fetch. This value would be split between selected “Entity types to fetch”. If the value is greater than 200, it will be considered as 200. The maximum is 200. (Default - 50) False
    Advanced: Minutes to look back when fetching Use this parameter to determine how long backward to look in the search for incidents that were created before the last run time and did not match the query when they were created. False
    Advanced: Fetch escalated Accounts and Hosts Use this parameter to fetch escalated Accounts and Hosts.

    Note: If this parameter is set to True, it will fetch Accounts and Hosts that have been updated based on the provided filter parameters, even if their detection timestamps are older than the first fetch time.
    False
    Re-Fetch Account/Host entity and Detection for new activity/event Whether to create multiple incidents for the same entity (Account or Host) or Detection if new detection activity occurs for the entity (Account or Host) or a new event is detected for the detection. False
  4. Click Test to validate the URLs, token, and connection.

Configuration for fetching Vectra Account or Vectra Host as a Cortex XSOAR incident

To fetch Vectra Account or Vectra Host as a Cortex XSOAR incident:

  1. Select Fetches incidents.
  2. Under Classifier, select “Vectra Detect”.
  3. Under Incident type, select “N/A”.
  4. Under Mapper (incoming), select “Vectra Detect - Incoming Mapper” for default mapping.
  5. Select the “Authentication Type” (Default - API Token).
  6. Enter connection parameters. (Vectra Detect FQDN or IP, API Token or Client ID and Client Secret Key)
  7. Select SSL certificate validation and Proxy if required.
  8. Update “Max created incidents per fetch” & “First fetch timestamp” based on your requirements.
  9. Select the Incident Mirroring Direction:
    1. Incoming - Mirrors changes from the Vectra into the Cortex XSOAR incident.
    2. Outgoing - Mirrors changes from the Cortex XSOAR incident to the Vectra.
    3. Incoming And Outgoing - Mirrors changes both Incoming and Outgoing directions on incidents.
  10. Check the “Re-Fetch closed incidents via mirroring” option if you want to prevent reopening of closed incidents and refetch them via mirroring on modification of the Vectra Account or Vectra Host.
  11. Enter the relevant tag name for mirror notes.
    Note: This value is mapped to the dbotMirrorTags incident field in Cortex XSOAR, which defines how Cortex XSOAR handles notes when you tag them in the War Room. This is required for mirroring notes from Cortex XSOAR to Vectra.
  12. Provide the filter parameter “Tags”, to filter entities by specific tag/s for fetch type account and host.
  13. Provide the filter parameter “Detection Category” and “Detection Type”, to filter detections by the specified category and type for fetch type account and host.
  14. Provide the “Minutes to look back when fetching” parameter to determine how long backward to look in the search for incidents that were created before the last run time and did not match the query when they were created.
  15. Check the “Fetch escalated Accounts and Hosts” option to fetch escalated Accounts and Hosts that have been updated based on the provided filter parameters, even if their detection timestamps are older than the first fetch time.
  16. Check the “Re-Fetch Account/Host entity and Detection for new activity/event” option if you want multiple Cortex XSOAR incidents to be created for the same Entity (Account or Host) or Detection whenever new detection activity is recorded. Disable this option if you want only one incident per Entity (Account or Host) or Detection regardless of ongoing activity.

Notes for mirroring:

  • The mirroring is strictly tied to incident types “Vectra Account” and “Vectra Host”, as well as the incoming mapper “Vectra Detect - Incoming Mapper”. If you want to change or use a custom incident type/mapper, ensure that related changes are also present.
  • The mirroring settings apply only for incidents that are fetched after applying the settings.
  • Any tags removed from the Vectra Account or Vectra Host will not be removed in the Cortex XSOAR incident, as Cortex XSOAR doesn’t allow the removal of the tags field via the backend. However, tags removed from the Cortex XSOAR incident UI will be removed from the Vectra Account or Vectra Host.
  • New notes from the Cortex XSOAR incident will be created as notes in the Vectra Account or Vectra Host. Updates to existing notes in the Cortex XSOAR incident will not be reflected in the Vectra Account or Vectra Host.
  • New notes from the Vectra Account or Vectra Host will be created as notes in the Cortex XSOAR incident. Updates to existing notes in the Vectra Account or Vectra Host will create new notes in the Cortex XSOAR incident.
  • If a closed Cortex XSOAR incident is tied to a specific Vectra Account or Vectra Host and new detections for that Vectra Account or Vectra Host arise or existing detections become active again:
    • If “Re-Fetch closed Incidents while Mirroring” checkbox is not selected and “Incoming Mirroring” is enabled, the incident will be automatically reopened.
    • If “Re-Fetch closed Incidents while Mirroring” checkbox is selected and “Outgoing Mirroring” is enabled, a new incident will be created for the Vectra Account or Vectra Host.
  • When a Cortex XSOAR incident is closed but there are still active detections on the Vectra side, and the Vectra Account or Vectra Host is subsequently updated:
    • If “Re-Fetch closed Incidents while Mirroring” checkbox is not selected and “Incoming Mirroring” is enabled, the corresponding XSOAR incident for that Vectra Account or Vectra Host will be reopened.
    • If “Re-Fetch closed Incidents while Mirroring” checkbox is selected and “Outgoing Mirroring” is enabled, a new incident will be created for the Vectra Account or Vectra Host.
  • If a Cortex XSOAR incident is reopened and the corresponding entity has an assignment in Vectra, the assignment will be removed from Vectra.
  • If you want to use the mirror mechanism and you’re using custom mappers, then the incoming mapper must contain the following fields: dbotMirrorDirection, dbotMirrorId, dbotMirrorInstance, and dbotMirrorTags.
  • To use a custom mapper, you must first duplicate the mapper and update the fields in the copy of the mapper. (Refer to the “Create a custom mapper consisting of the default Vectra Detect - Incoming Mapper” section for more information.)
  • Following new fields are introduced in the response of the incident to enable the mirroring:
    • mirror_direction: This field determines the mirroring direction for the incident. It is a required field for Cortex XSOAR to enable mirroring support.
    • mirror_tags: This field determines what would be the tag needed to mirror the Cortex XSOAR entry out to Vectra. It is a required field for XSOAR to enable mirroring support.
    • mirror_instance: This field determines from which instance the Cortex XSOAR incident was created. It is a required field for Cortex XSOAR to enable mirroring support.

Cleanup Duplicate Incidents

  • Use the Close All Duplicate XSOAR Incidents - Vectra Detect playbook to clean up duplicate incidents. You can use VectraDetectCloseDuplicateIncidents script individually to clean up duplicate incidents.
  • You can also schedule a job with Close All Duplicate XSOAR Incidents - Vectra Detect playbook in Cortex XSOAR to clean up incidents periodically. Refer to this Cortex XSOAR documentation for more information.

Commands

You can execute these commands from the Cortex XSOAR 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.

vectra-search-accounts


Returns a list of Account objects. All search attributes will be cumulative unless you’re using the search_query_only one, in that case, only this one will be taken into account.

Base Command

vectra-search-accounts

Input

Argument Name Description Required
min_id Returns Accounts with an ID greater than or equal to the specified ID. Optional
max_id Returns Accounts with an ID less than or equal to the specified ID. Optional
min_threat Returns Accounts with a threat score greater than or equal to the specified score. Optional
max_threat Returns Accounts with a threat score less than or equal to the specified score. Optional
min_certainty Returns Accounts with a certainty score greater than or equal to the specified score. Optional
max_certainty Returns Accounts with a certainty score less than or equal to the specified score. Optional
state Filters by state (‘active’, ‘inactive’). Possible values are: active, inactive. Optional
search_query Search query in Lucene query syntax. Optional
search_query_only Use specifically this search query. Compared to “search_query” where default arguments are appended. Optional
min_privilege_level Returns entries with a privilege level greater than or equal to the specified score. Optional
max_privilege_level Returns entries with a privilege level greater than or equal to the specified score. Optional
privilege_category Filters by the privilege category (“low”, “medium”, “high”) provided. Optional
tags Filters by a tag or a comma-separated list tags. Optional

Context Output

Path Type Description
Vectra.Account.Assignee String Vectra user account this Account is assigned to
Vectra.Account.AssignedDate String Assignment date
Vectra.Account.CertaintyScore Number Account certainty score
Vectra.Account.ID Number Account ID (unique)
Vectra.Account.LastDetectionTimestamp String Last time a detection linked to this account has been seen
Vectra.Account.PrivilegeLevel Number Account privilege level (from 1 to 10)
Vectra.Account.PrivilegeCategory String Account privilege category (Either ‘Low’, ‘Medium’ or ‘High’ - Privilege levels of 1-2 > ‘Low’, 3-7 > ‘Medium’, 8-10 > ‘High’)
Vectra.Account.Severity String Account severity according to scores (‘Low’, ‘Medium’, ‘High’, ‘Critical’)
Vectra.Account.State String Account state (‘active’, ‘inactive’)
Vectra.Account.Tags String Account tags
Vectra.Account.ThreatScore Number Account threat score
Vectra.Account.Type String Account type (‘kerberos’ or ‘o365’)
Vectra.Account.URL String Account URL to pivot to Vectra UI
Vectra.Account.Name String The username of the account

vectra-search-hosts


Returns a list of Host objects. All search attributes will be cumulative unless you’re using the search_query_only one, in that case, only this one will be taken into account.

Base Command

vectra-search-hosts

Input

Argument Name Description Required
min_id Returns Hosts with an ID greater than or equal to the specified ID. Optional
max_id Returns Hosts with an ID less than or equal to the specified ID. Optional
min_threat Returns Hosts with a threat score greater than or equal to the specified score. Optional
max_threat Returns Hosts with a threat score less than or equal to the specified score. Optional
min_certainty Returns Hosts with a certainty score greater than or equal to the specified score. Optional
max_certainty Returns Hosts with a certainty score less than or equal to the specified score. Optional
state Filters by state (‘active’, ‘inactive’). Possible values are: active, inactive. Optional
search_query Search query in Lucene query syntax. Optional
search_query_only Use specifically this search query. Compared to “search_query” where default arguments are appended. Optional

Context Output

Path Type Description
Vectra.Host.Assignee String Vectra user account this Host is assigned to
Vectra.Host.AssignedDate String Assignment date
Vectra.Host.CertaintyScore Number Host certainty score
Vectra.Host.HasActiveTraffic Boolean Whether this Host has active traffic
Vectra.Host.Hostname String Host name
Vectra.Host.ID Number Host ID (Unique)
Vectra.Host.IP String Host IP address
Vectra.Host.IsKeyAsset Boolean Whether this Host is seen as a key asset
Vectra.Host.IsTargetingKeyAsset Boolean Whether this Host is targeting a key asset
Vectra.Host.PrivilegeLevel Number Host privilege level (from 1 to 10)
Vectra.Host.PrivilegeCategory String Host privilege category. (Either ‘Low’, ‘Medium’ or ‘High’ - Privilege levels of 1-2 > ‘Low’, 3-7 > ‘Medium’, 8-10 > ‘High’)
Vectra.Host.ProbableOwner String Host probable owner
Vectra.Host.SensorLUID String Sensor LUID that saw this Host
Vectra.Host.SensorName String Sensor Name that saw this Host
Vectra.Host.Sensor String Sensor details that have seen this Host
Vectra.Host.Severity String Host severity according to scores (‘Low’, ‘Medium’, ‘High’, ‘Critical’)
Vectra.Host.State String Host state (‘active’, ‘inactive’)
Vectra.Host.Tags String Host tags
Vectra.Host.ThreatScore Number Host threat score
Vectra.Host.URL String Host URL to pivot to Vectra UI

vectra-search-detections


Returns a list of Detection objects. All search attributes will be cumulative unless you’re using the search_query_only one, in that case, only this one will be taken into account.

Base Command

vectra-search-detections

Input

Argument Name Description Required
min_id Returns Detections with an ID greater than or equal to the specified ID. Optional
max_id Returns Detections with an ID less than or equal to the specified ID. Optional
min_threat Returns Detections with a threat score greater than or equal to the specified score. Optional
max_threat Returns Detections with a threat score less than or equal to the specified score. Optional
min_certainty Returns Detections with a certainty score greater than or equal to the specified score. Optional
max_certainty Returns Detections with a certainty score less than or equal to the specified score. Optional
state Filters by state (‘active’, ‘inactive’). Possible values are: active, inactive. Optional
search_query Search query in Lucene query syntax. Optional
search_query_only Use specifically this search query. Compared to “search_query” where default arguments are appended. Optional

Context Output

Path Type Description
Vectra.Detection.Assignee String Vectra user account this detection is assigned to
Vectra.Detection.AssignedDate String Assignment date
Vectra.Detection.Category String Detection category (Lateral, Exfil, …)
Vectra.Detection.CertaintyScore Number Detection certainty score
Vectra.Detection.Description String Detection description
Vectra.Detection.DestinationIPs String Detection destination IPs
Vectra.Detection.DestinationPorts String Detection destination ports
Vectra.Detection.FirstTimestamp String First time this detection has been seen
Vectra.Detection.ID Number Detection ID (unique)
Vectra.Detection.IsTargetingKeyAsset Boolean Whether this detection is targeting a key asset
Vectra.Detection.LastTimestamp String Last time this detection has been seen
Vectra.Detection.Name String The name of the detection. Would be a user defined name if this detection is triaged or the default type name instead
Vectra.Detection.Severity String Detection severity according to scores (‘Low’, ‘Medium’, ‘High’, ‘Critical’)
Vectra.Detection.SensorLUID String Sensor LUID that saw this detection
Vectra.Detection.SensorName String Sensor name that saw this detection.
Vectra.Detection.SourceAccountID String Account ID relating to this detection
Vectra.Detection.SourceHostID String Host ID relating to this detection
Vectra.Detection.SourceIP String Source IP relating to this detection
Vectra.Detection.State String Detection state (‘active’, ‘inactive’)
Vectra.Detection.Tags String Detection tags
Vectra.Detection.ThreatScore Number Detection threat score
Vectra.Detection.TriageRuleID String Triage rule ID related to this detection
Vectra.Detection.Type String Detection type (Brute Force, Port Sweep, …)
Vectra.Detection.URL String Detection URL to pivot to Vectra UI

vectra-search-assignments


Return a list of assignments. By default already resolved assignment are not returned.

Base Command

vectra-search-assignments

Input

Argument Name Description Required
account_ids Filters by accounts IDs. Optional
assignee_ids Filters by assignees IDs. Optional
host_ids Filters by hosts IDs. Optional
outcome_ids Filters by outcomes IDs. Optional
resolved Filters by resolution state. Optional

Context Output

Path Type Description
Vectra.Assignment.AccountID Number Account ID this assignment is linked to
Vectra.Assignment.AssignedBy String Who lastly assigned this assignment
Vectra.Assignment.AssignedDate String When this assignment was lastly assigned
Vectra.Assignment.AssignedTo String To who this assignment is assigned
Vectra.Assignment.HostID String Host ID this assignment is linked to
Vectra.Assignment.ID Number Assignment ID (unique)
Vectra.Assignment.IsResolved Boolean Is this assignment resolved
Vectra.Assignment.OutcomeCategory String Assignment Outcome category
Vectra.Assignment.OutcomeTitle String Assignment Outcome title
Vectra.Assignment.TriagedDetections String List of Detection that have been triaged with the resolution
Vectra.Assignment.TriagedAs String Name of the triage rule if any
Vectra.Assignment.ResolvedBy String Who resolved this assignment
Vectra.Assignment.ResolvedDate string When this assignment was resolved

vectra-search-users


Returns a list of Vectra Users. All search attributes will be cumulative.

Base Command

vectra-search-users

Input

Argument Name Description Required
username Filters by user name. Optional
role Filters by user role. Optional
type Filters by type (‘Local’, ‘SAML’, …). Possible values are: local, SAML. Optional
last_login_datetime Filters for Users that logged in since the given datetime. Optional

Context Output

Path Type Description
Vectra.User.Email String User’s email address
Vectra.User.ID Number User ID (unique)
Vectra.User.Role String User’s role
Vectra.User.Type String User type (‘Local’, ‘SAML’, …)
Vectra.User.Username String Username
Vectra.User.LastLoginDate String User’s last login datetime

vectra-search-outcomes


Returns a list of assignment outcomes.

Base Command

vectra-search-outcomes

Input

There are no input arguments for this command.

Context Output

Path Type Description
Vectra.Outcome.IsBuiltIn String Is this Outcome a builtin Outcome
Vectra.Outcome.Category String Outcome’s category (‘False Positive’, ‘Benign True Positive’, ‘Malicious True Positive’)
Vectra.Outcome.ID Number Outcome ID (unique)
Vectra.Outcome.Title String Outcome title

vectra-account-describe


Returns a single Account details

Base Command

vectra-account-describe

Input

Argument Name Description Required
id Account ID you want to get details on. Optional

Context Output

Path Type Description
Vectra.Account.Assignee String Vectra user account this Account is assigned to
Vectra.Account.AssignedDate String Assignment date
Vectra.Account.CertaintyScore Number Account certainty score
Vectra.Account.ID Number Account ID (unique)
Vectra.Account.LastDetectionTimestamp String Last time a detection linked to this account has been seen
Vectra.Account.PrivilegeLevel Number Account privilege level (from 1 to 10)
Vectra.Account.PrivilegeCategory String Account privilege category (Either ‘Low’, ‘Medium’ or ‘High’ - Privilege levels of 1-2 > ‘Low’, 3-7 > ‘Medium’, 8-10 > ‘High’)
Vectra.Account.Severity String Account severity according to scores (‘Low’, ‘Medium’, ‘High’, ‘Critical’)
Vectra.Account.State String Account state (‘active’, ‘inactive’)
Vectra.Account.Tags String Account tags
Vectra.Account.ThreatScore Number Account threat score
Vectra.Account.Type String Account type (‘kerberos’ or ‘o365’)
Vectra.Account.URL String Account URL to pivot to Vectra UI
Vectra.Account.Name String The username of the account

vectra-account-add-tags


Add tags to an Account

Base Command

vectra-account-add-tags

Input

Argument Name Description Required
id Account ID you want to add tags on. Optional
tags The tags list (comma separated). Optional

Context Output

There is no context output for this command.

vectra-account-del-tags


Delete tags from an Account

Base Command

vectra-account-del-tags

Input

Argument Name Description Required
id Account ID you want to del tags from. Optional
tags The tags list (comma separated). Optional

Context Output

There is no context output for this command.

vectra-account-tag-list


Returns a list of tags for a specified account.

Base Command

vectra-account-tag-list

Input

Argument Name Description Required
id Specify the ID of the account. Required

Context Output

Path Type Description
Vectra.Account.ID Number ID of the account associated with the tags.
Vectra.Account.Tags String Tags associated to the account.

Command example

!vectra-account-tag-list id="2"

Context Example

{
    "Vectra.Account": {
        "ID": 2,
        "Tags": [
            "note",
            "tag_from_xsoar",
            "tag_from_vectra"
        ]
    }
}

Human Readable Output

List of tags: note, tag_from_xsoar, tag_from_vectra

vectra-account-note-add


Add a note to the account.

Base Command

vectra-account-note-add

Input

Argument Name Description Required
account_id Specify the ID of the account. Required
note Note to be added in the specified account_id. Required

Context Output

Path Type Description
Vectra.Account.Notes.account_id Number ID of the account associated with the note.
Vectra.Account.Notes.note_id Number ID of the note.
Vectra.Account.Notes.date_created Date Date when the note was created.
Vectra.Account.Notes.date_modified Date Date when the note was last modified.
Vectra.Account.Notes.created_by String User who created the note.
Vectra.Account.Notes.modified_by String User who last modified the note.
Vectra.Account.Notes.note String Content of the note.

Command example

!vectra-account-note-add account_id="2" note="test note"

Context Example

{
    "Vectra.Account.Notes": {
        "date_created": "2024-07-10T07:30:58.574942Z",
        "created_by": "xsoar",
        "note": "test note",
        "note_id": 1959,
        "account_id": 2
    }
}

Human Readable Output

The note has been successfully added to the account

Returned Note ID: 1959

vectra-account-note-update


Update a note in the account.

Base Command

vectra-account-note-update

Input

Argument Name Description Required
account_id Specify the ID of the account. Required
note_id Specify the ID of the note.

Note: Use the vectra-account-note-list command to get note_id.
Required
note Note to be updated for the specified note_id. Required

Context Output

Path Type Description
Vectra.Account.Notes.account_id Number ID of the account associated with the note.
Vectra.Account.Notes.note_id Number ID of the note.
Vectra.Account.Notes.date_created Date Date when the note was created.
Vectra.Account.Notes.date_modified Date Date when the note was last modified.
Vectra.Account.Notes.created_by String User who created the note.
Vectra.Account.Notes.modified_by String User who last modified the note.
Vectra.Account.Notes.note String Content of the note.

Command example

!vectra-account-note-update account_id="2" note_id="1959" note="updated test note"

Context Example

{
    "Vectra.Account.Notes": {
        "date_created": "2024-07-10T07:30:58.574942Z",
        "date_modified": "2024-07-12T06:42:29.546835Z",
        "created_by": "xsoar",
        "modified_by": "xsoar",
        "note": "updated test note",
        "note_id": 1959,
        "account_id": 2
    }
}

Human Readable Output

The note has been successfully updated in the account

vectra-account-note-remove


Remove a note from the account.

Base Command

vectra-account-note-remove

Input

Argument Name Description Required
account_id Specify the ID of the account. Required
note_id Specify the ID of the note.

Note: Use the vectra-account-note-list command to get note_id.
Required

Context Output

There is no context output for this command.

Command example

!vectra-account-note-remove account_id="2" note_id="1959"

Human Readable Output

The note has been successfully removed from the account

vectra-account-note-list


List all notes of the specific account.

Base Command

vectra-account-note-list

Input

Argument Name Description Required
account_id Specify the ID of the account. Required

Context Output

Path Type Description
Vectra.Account.Notes.account_id Number ID of the account associated with the note.
Vectra.Account.Notes.note_id Number ID of the note.
Vectra.Account.Notes.date_created Date Date when the note was created.
Vectra.Account.Notes.date_modified Date Date when the note was last modified.
Vectra.Account.Notes.created_by String User who created the note.
Vectra.Account.Notes.modified_by String User who last modified the note.
Vectra.Account.Notes.note String Content of the note.

Command example

!vectra-account-note-list account_id="2"

Context Example

{
    "Vectra.Account.Notes": [
        {
            "date_created": "2024-07-10T05:40:31Z",
            "date_modified": "2024-07-16T12:56:30Z",
            "created_by": "xsoar",
            "modified_by": "xsoar",
            "note": "updated_note",
            "note_id": 1959,
            "account_id": 2
        },
        {
            "date_created": "2024-07-08T07:11:49Z",
            "created_by": "xsoar",
            "note": "Here comes your note TEST",
            "note_id": 1906,
            "account_id": 2
        }
    ]
}

Human Readable Output

Notes Table
Note ID Note Created By Created Date Modified By Modified Date
1959 updated_note xsoar 2024-07-10T05:40:31Z xsoar 2024-07-16T12:56:30Z
1906 Here comes your note TEST xsoar 2024-07-08T07:11:49Z    

vectra-account-markall-detections-asfixed


Mark active detections as fixed by providing the ID of the account in the argument.

Base Command

vectra-account-markall-detections-asfixed

Input

Argument Name Description Required
account_id Provide an account ID. Required

Context Output

There is no context output for this command.

Command example

!vectra-account-markall-detections-asfixed account_id=109

Human Readable Output

The active detections of the provided account have been successfully marked as fixed.

vectra-account-markall-detections-asclosed


Mark active detections as closed by providing the ID of the account in the argument.

Base Command

vectra-account-markall-detections-asclosed

Input

Argument Name Description Required
account_id Provide an account ID. Required
close_reason Provide the close reason. Possible values are: benign, remediated. Required

Context Output

There is no context output for this command.

Command example

!vectra-account-markall-detections-asclosed account_id=108 close_reason=benign

Human Readable Output

The active detections of the provided account have been successfully closed as benign

vectra-host-describe


Returns a single Host details

Base Command

vectra-host-describe

Input

Argument Name Description Required
id Host ID you want to get details on. Optional

Context Output

Path Type Description
Vectra.Host.Assignee String Vectra user account this Host is assigned to
Vectra.Host.AssignedDate String Assignment date
Vectra.Host.CertaintyScore Number Host certainty score
Vectra.Host.HasActiveTraffic Boolean Whether this Host has active traffic
Vectra.Host.Hostname String Host name
Vectra.Host.ID Number Host ID (Unique)
Vectra.Host.IP String Host IP address
Vectra.Host.IsKeyAsset Boolean Whether this Host is seen as a key asset
Vectra.Host.IsTargetingKeyAsset Boolean Whether this Host is targeting a key asset
Vectra.Host.PrivilegeLevel Number Host privilege level (from 1 to 10)
Vectra.Host.PrivilegeCategory String Host privilege category. (Either ‘Low’, ‘Medium’ or ‘High’ - Privilege levels of 1-2 > ‘Low’, 3-7 > ‘Medium’, 8-10 > ‘High’)
Vectra.Host.ProbableOwner String Host probable owner
Vectra.Host.SensorLUID String Sensor LUID that saw this Host
Vectra.Host.SensorName String Sensor Name that saw this Host
Vectra.Host.Sensor String Sensor details that have seen this Host
Vectra.Host.Severity String Host severity according to scores (‘Low’, ‘Medium’, ‘High’, ‘Critical’)
Vectra.Host.State String Host state (‘active’, ‘inactive’)
Vectra.Host.Tags String Host tags
Vectra.Host.ThreatScore Number Host threat score
Vectra.Host.URL String Host URL to pivot to Vectra UI

vectra-host-add-tags


Add tags to an Host

Base Command

vectra-host-add-tags

Input

Argument Name Description Required
id Host ID you want to add tags on. Optional
tags The tags list (comma separated). Optional

Context Output

There is no context output for this command.

vectra-host-del-tags


Delete tags from an Host

Base Command

vectra-host-del-tags

Input

Argument Name Description Required
id Host ID you want to del tags from. Optional
tags The tags list (comma separated). Optional

Context Output

There is no context output for this command.

vectra-host-tag-list


Returns a list of tags for a specified host.

Base Command

vectra-host-tag-list

Input

Argument Name Description Required
id Specify the ID of the host. Required

Context Output

Path Type Description
Vectra.Host.ID Number ID of the host associated with the tags.
Vectra.Host.Tags String Tags associated to the host.

Command example

!vectra-host-tag-list id="2"

Context Example

{
    "Vectra.Host": {
        "ID": 2,
        "Tags": [
            "note",
            "tag_from_xsoar",
            "tag_from_vectra"
        ]
    }
}

Human Readable Output

List of tags: note, tag_from_xsoar, tag_from_vectra

vectra-host-note-add


Add a note to the host.

Base Command

vectra-host-note-add

Input

Argument Name Description Required
host_id Specify the ID of the host. Required
note Note to be added in the specified host_id. Required

Context Output

Path Type Description
Vectra.Host.Notes.host_id Number ID of the host associated with the note.
Vectra.Host.Notes.note_id Number ID of the note.
Vectra.Host.Notes.date_created Date Date when the note was created.
Vectra.Host.Notes.date_modified Date Date when the note was last modified.
Vectra.Host.Notes.created_by String User who created the note.
Vectra.Host.Notes.modified_by String User who last modified the note.
Vectra.Host.Notes.note String Content of the note.

Command example

!vectra-host-note-add host_id="5" note="test note"

Context Example

{
    "Vectra.Host.Notes": {
        "date_created": "2024-07-10T07:31:58.574942Z",
        "created_by": "xsoar",
        "note": "test note",
        "note_id": 1960,
        "host_id": 5
    }
}

Human Readable Output

The note has been successfully added to the host

Returned Note ID: 1960

vectra-host-note-update


Update a note in the host.

Base Command

vectra-host-note-update

Input

Argument Name Description Required
host_id Specify the ID of the host. Required
note_id Specify the ID of the note.

Note: Use the vectra-host-note-list command to get note_id.
Required
note Note to be updated for the specified note_id. Required

Context Output

Path Type Description
Vectra.Host.Notes.host_id Number ID of the host associated with the note.
Vectra.Host.Notes.note_id Number ID of the note.
Vectra.Host.Notes.date_created Date Date when the note was created.
Vectra.Host.Notes.date_modified Date Date when the note was last modified.
Vectra.Host.Notes.created_by String User who created the note.
Vectra.Host.Notes.modified_by String User who last modified the note.
Vectra.Host.Notes.note String Content of the note.

Command example

!vectra-account-note-update host_id="7" note_id="1960" note="updated test note"

Context Example

{
    "Vectra.Host.Notes": {
        "date_created": "2024-07-10T07:31:58.574942Z",
        "date_modified": "2024-07-12T06:44:29.546835Z",
        "created_by": "xsoar",
        "modified_by": "xsoar",
        "note": "updated test note",
        "note_id": 1960,
        "host_id": 7
    }
}

Human Readable Output

The note has been successfully updated in the host

vectra-host-note-remove


Remove a note from the host.

Base Command

vectra-host-note-remove

Input

Argument Name Description Required
host_id Specify the ID of the host. Required
note_id Specify the ID of the note.

Note: Use the vectra-host-note-list command to get note_id.
Required

Context Output

There is no context output for this command.

Command example

!vectra-host-note-remove host_id="7" note_id="1960"

Human Readable Output

The note has been successfully removed from the host

vectra-host-note-list


List all notes of the specific host.

Base Command

vectra-host-note-list

Input

Argument Name Description Required
host_id Specify the ID of the host. Required

Context Output

Path Type Description
Vectra.Host.Notes.host_id Number ID of the host associated with the note.
Vectra.Host.Notes.note_id Number ID of the note.
Vectra.Host.Notes.date_created Date Date when the note was created.
Vectra.Host.Notes.date_modified Date Date when the note was last modified.
Vectra.Host.Notes.created_by String User who created the note.
Vectra.Host.Notes.modified_by String User who last modified the note.
Vectra.Host.Notes.note String Content of the note.

Command example

!vectra-host-note-list host_id="7"

Context Example

{
    "Vectra.Host.Notes": [
        {
            "date_created": "2024-07-11T07:32:31Z",
            "created_by": "xsoar",
            "note": "test note",
            "note_id": 1960,
            "host_id": 7
        },
        {
            "date_created": "2024-07-11T06:23:07Z",
            "created_by": "cds_xsoar",
            "note": "test note",
            "note_id": 1982,
            "host_id": 7
        }
    ]
}

Human Readable Output

Notes Table
Note ID Note Created By Created Date
1960 test note xsoar 2024-07-11T07:32:31Z
1982 test note cds_xsoar 2024-07-11T06:23:07Z

vectra-host-markall-detections-asfixed


Mark active detections as fixed by providing ID of the host in the argument.

Base Command

vectra-host-markall-detections-asfixed

Input

Argument Name Description Required
host_id Provide a host ID. Required

Context Output

There is no context output for this command.

Command example

!vectra-host-markall-detections-asfixed host_id=23176

Human Readable Output

The active detections of the provided host have been successfully marked as fixed.

vectra-host-markall-detections-asclosed


Mark active detections as closed by providing the ID of the host in the argument.

Base Command

vectra-host-markall-detections-asclosed

Input

Argument Name Description Required
host_id Provide a host ID. Required
close_reason Provide the close reason. Possible values are: benign, remediated. Required

Context Output

There is no context output for this command.

Command example

!vectra-host-markall-detections-asclosed host_id=23176 close_reason=remediated

Human Readable Output

The active detections of the provided host have been successfully closed as remediated

vectra-detection-describe


Returns a single detection details

Base Command

vectra-detection-describe

Input

Argument Name Description Required
id Detection ID you want to get details on. Optional

Context Output

Path Type Description
Vectra.Detection.Assignee String Vectra user account this detection is assigned to
Vectra.Detection.AssignedDate String Assignment date
Vectra.Detection.Category String Detection category (Lateral, Exfil, …)
Vectra.Detection.CertaintyScore Number Detection certainty score
Vectra.Detection.Description String Detection description
Vectra.Detection.DestinationIPs String Detection destination IPs
Vectra.Detection.DestinationPorts String Detection destination ports
Vectra.Detection.FirstTimestamp String First time this detection has been seen
Vectra.Detection.ID Number Detection ID (unique)
Vectra.Detection.IsTargetingKeyAsset Boolean Whether this detection is targeting a key asset
Vectra.Detection.LastTimestamp String Last time this detection has been seen
Vectra.Detection.Name String The name of the detection. Would be a user defined name if this detection is triaged or the default type name instead
Vectra.Detection.Severity String Detection severity according to scores (‘Low’, ‘Medium’, ‘High’, ‘Critical’)
Vectra.Detection.SensorLUID String Sensor LUID that saw this detection
Vectra.Detection.SensorName String Sensor name that saw this detection.
Vectra.Detection.SourceAccountID String Account ID relating to this detection
Vectra.Detection.SourceHostID String Host ID relating to this detection
Vectra.Detection.SourceIP String Source IP relating to this detection
Vectra.Detection.State String Detection state (‘active’, ‘inactive’)
Vectra.Detection.Tags String Detection tags
Vectra.Detection.ThreatScore Number Detection threat score
Vectra.Detection.TriageRuleID String Triage rule ID related to this detection
Vectra.Detection.Type String Detection type (Brute Force, Port Sweep, …)
Vectra.Detection.URL String Detection URL to pivot to Vectra UI

vectra-detection-get-pcap


Returns a Detection’s PCAP file (if available)

Base Command

vectra-detection-get-pcap

Input

Argument Name Description Required
id The Detection ID you want to get the PCAP file from. Optional

Context Output

There is no context output for this command.

vectra-detection-markasfixed


Marks/Unmarks a Detection as fixed by providing the Detection ID

Base Command

vectra-detection-markasfixed

Input

Argument Name Description Required
id Detection ID you want to mark/unmark as fixed. Optional
fixed The wanted detection status (“true”, “false”). No default value. Possible values are: true, false. Optional

Context Output

There is no context output for this command.

vectra-detection-add-tags


Add tags to a Detection

Base Command

vectra-detection-add-tags

Input

Argument Name Description Required
id Detection ID you want to add tags on. Optional
tags The tags list (comma separated). Optional

Context Output

There is no context output for this command.

vectra-detection-del-tags


Delete tags from a Detection

Base Command

vectra-detection-del-tags

Input

Argument Name Description Required
id Detection ID you want to del tags from. Optional
tags The tags list (comma separated). Optional

Context Output

There is no context output for this command.

vectra-detection-tag-list


Returns a list of tags for a specified detection.

Base Command

vectra-detection-tag-list

Input

Argument Name Description Required
id Specify the ID of the detection. Required

Context Output

Path Type Description
Vectra.Detection.ID Number ID of the detection associated with the tags.
Vectra.Detection.Tags String Tags associated to the detection.

Command example

!vectra-detection-tag-list id="2"

Context Example

{
    "Vectra.Detection": {
        "ID": 2,
        "Tags": [
            "note",
            "tag_from_xsoar",
            "tag_from_vectra"
        ]
    }
}

Human Readable Output

List of tags: note, tag_from_xsoar, tag_from_vectra

vectra-detection-note-add


Add a note to the detection.

Base Command

vectra-detection-note-add

Input

Argument Name Description Required
detection_id Specify the ID of the detection. Required
note Note to be added in the specified detection_id. Required

Context Output

Path Type Description
Vectra.Detection.Notes.detection_id Number ID of the detection associated with the note.
Vectra.Detection.Notes.note_id Number ID of the note.
Vectra.Detection.Notes.date_created Date Date when the note was created.
Vectra.Detection.Notes.date_modified Date Date when the note was last modified.
Vectra.Detection.Notes.created_by String User who created the note.
Vectra.Detection.Notes.modified_by String User who last modified the note.
Vectra.Detection.Notes.note String Content of the note.

Command example

!vectra-detection-note-add detection_id="7" note="test note"

Context Example

{
    "Vectra.Detection.Notes": {
        "date_created": "2024-07-10T07:32:58.574942Z",
        "created_by": "xsoar",
        "note": "test note",
        "note_id": 1961,
        "detection_id": 7
    }
}

Human Readable Output

The note has been successfully added to the detection

Returned Note ID: 1961

vectra-detection-note-update


Update a note in the detection.

Base Command

vectra-detection-note-update

Input

Argument Name Description Required
detection_id Specify the ID of the detection. Required
note_id Specify the ID of the note.

Note: Use the vectra-detection-note-list command to get note_id.
Required
note Note to be updated for the specified note_id. Required

Context Output

Path Type Description
Vectra.Detection.Notes.detection_id Number ID of the detection associated with the note.
Vectra.Detection.Notes.note_id Number ID of the note.
Vectra.Detection.Notes.date_created Date Date when the note was created.
Vectra.Detection.Notes.date_modified Date Date when the note was last modified.
Vectra.Detection.Notes.created_by String User who created the note.
Vectra.Detection.Notes.modified_by String User who last modified the note.
Vectra.Detection.Notes.note String Content of the note.

Command example

!vectra-detection-note-update detection_id="9" note_id="1961" note="updated test note"

Context Example

{
    "Vectra.Detection.Notes": {
        "date_created": "2024-07-10T07:32:58.574942Z",
        "date_modified": "2024-07-12T06:43:29.546835Z",
        "created_by": "xsoar",
        "modified_by": "xsoar",
        "note": "updated test note",
        "note_id": 1961,
        "detection_id": 9
    }
}

Human Readable Output

The note has been successfully updated in the detection

vectra-detection-note-remove


Remove a note from the detection.

Base Command

vectra-detection-note-remove

Input

Argument Name Description Required
detection_id Specify the ID of the detection. Required
note_id Specify the ID of the note.

Note: Use the vectra-detection-note-list command to get note_id.
Required

Context Output

There is no context output for this command.

Command example

!vectra-detection-note-remove detection_id=97" note_id="1961"

Human Readable Output

The note has been successfully removed from the detection

vectra-detection-note-list


List all notes of the specific detection.

Base Command

vectra-detection-note-list

Input

Argument Name Description Required
detection_id Specify the ID of the detection. Required

Context Output

Path Type Description
Vectra.Detection.Notes.detection_id Number ID of the detection associated with the note.
Vectra.Detection.Notes.note_id Number ID of the note.
Vectra.Detection.Notes.date_created Date Date when the note was created.
Vectra.Detection.Notes.date_modified Date Date when the note was last modified.
Vectra.Detection.Notes.created_by String User who created the note.
Vectra.Detection.Notes.modified_by String User who last modified the note.
Vectra.Detection.Notes.note String Content of the note.

Command example

!vectra-detection-note-list detection_id="9"

Context Example

{
    "Vectra.Detection.Notes": [
        {
            "date_created": "2024-07-12T04:52:20Z",
            "date_modified": "2024-07-12T10:21:03Z",
            "created_by": "xsoar",
            "modified_by": "xsoar",
            "note": "updated note 2nd",
            "note_id": 1961,
            "detection_id": 9
        },
        {
            "date_created": "2024-07-11T07:32:20Z",
            "created_by": "xsoar",
            "note": "your first test note",
            "note_id": 1937,
            "detection_id": 9
        }
    ]
}

Human Readable Output

Notes Table
Note ID Note Created By Created Date Modified By Modified Date
1961 updated note 2nd xsoar 2024-07-12T04:52:20Z xsoar 2024-07-12T10:21:03Z
1937 your first test note xsoar 2024-07-11T07:32:20Z    

vectra-detections-mark-asclosed


Mark detections as closed with provided detection IDs in the argument.

Base Command

vectra-detections-mark-asclosed

Input

Argument Name Description Required
detection_ids Provide a list of detection IDs separated by commas or a single detection ID. Required
close_reason Provide the close reason. Possible values are: benign, remediated. Required

Context Output

There is no context output for this command.

Command example

!vectra-detections-mark-asclosed detection_ids=123,345 close_reason=remediated

Human Readable Output

The provided detection IDs have been successfully closed as remediated

vectra-detections-mark-asopen


Open detections with provided detection IDs in the argument.

Base Command

vectra-detections-mark-asopen

Input

Argument Name Description Required
detection_ids Provide a list of detection IDs separated by commas or a single detection ID. Required

Context Output

There is no context output for this command.

Command example

!vectra-detections-mark-asopen detection_ids=123,345

Human Readable Output

The provided detection IDs have been successfully re-opened

vectra-outcome-describe


Returns a single outcome details

Base Command

vectra-outcome-describe

Input

Argument Name Description Required
id Outcome ID you want to get details on. Optional

Context Output

Path Type Description
Vectra.Outcome.IsBuiltIn String Is this Outcome a builtin Outcome
Vectra.Outcome.Category String Outcome’s category (‘False Positive’, ‘Benign True Positive’, ‘Malicious True Positive’)
Vectra.Outcome.ID Number Outcome ID (unique)
Vectra.Outcome.Title String Outcome title

vectra-outcome-create


Creates a new assignment outcome

Base Command

vectra-outcome-create

Input

Argument Name Description Required
title Outcome title (will be visible in the UI). Optional
category Outcome category (one of the 3). Possible values are: Benign True Positive, Malicious True Positive, False Positive. Optional

Context Output

Path Type Description
Vectra.Outcome.IsBuiltIn String Is this Outcome a builtin Outcome
Vectra.Outcome.Category String Outcome’s category (‘False Positive’, ‘Benign True Positive’, ‘Malicious True Positive’)
Vectra.Outcome.ID Number Outcome ID (unique)
Vectra.Outcome.Title String Outcome title

vectra-assignment-describe


Returns a single assignment details

Base Command

vectra-assignment-describe

Input

Argument Name Description Required
id Assignment ID you want to get details on. Optional

Context Output

Path Type Description
Vectra.Assignment.AccountID Number Account ID this assignment is linked to
Vectra.Assignment.AssignedBy String Who lastly assigned this assignment
Vectra.Assignment.AssignedDate String When this assignment was lastly assigned
Vectra.Assignment.AssignedTo String To who this assignment is assigned
Vectra.Assignment.HostID String Host ID this assignment is linked to
Vectra.Assignment.ID Number Assignment ID (unique)
Vectra.Assignment.IsResolved Boolean Is this assignment resolved
Vectra.Assignment.OutcomeCategory String Assignment Outcome category
Vectra.Assignment.OutcomeTitle String Assignment Outcome title
Vectra.Assignment.TriagedDetections String List of Detection that have been triaged with the resolution
Vectra.Assignment.TriagedAs String Name of the triage rule if any
Vectra.Assignment.ResolvedBy String Who resolved this assignment
Vectra.Assignment.ResolvedDate string When this assignment was resolved

vectra-assignment-assign


Assigns an Account/Host entity to a Vectra User for investigation. If an assignment already exists on this entity, it will be reassigned

Base Command

vectra-assignment-assign

Input

Argument Name Description Required
assignee_id Assignee’s ID (Vectra User ID). Optional
assignment_id Assignment ID if an assignment already exists for the given entity. Optional
account_id Account ID. Optional
host_id Host ID. Optional

Context Output

Path Type Description
Vectra.Assignment.AccountID Number Account ID this assignment is linked to
Vectra.Assignment.AssignedBy String Who lastly assigned this assignment
Vectra.Assignment.AssignedDate String When this assignment was lastly assigned
Vectra.Assignment.AssignedTo String To who this assignment is assigned
Vectra.Assignment.HostID String Host ID this assignment is linked to
Vectra.Assignment.ID Number Assignment ID (unique)
Vectra.Assignment.IsResolved Boolean Is this assignment resolved
Vectra.Assignment.OutcomeCategory String Assignment Outcome category
Vectra.Assignment.OutcomeTitle String Assignment Outcome title
Vectra.Assignment.TriagedDetections String List of Detection that have been triaged with the resolution
Vectra.Assignment.TriagedAs String Name of the triage rule if any
Vectra.Assignment.ResolvedBy String Who resolved this assignment
Vectra.Assignment.ResolvedDate string When this assignment was resolved

vectra-assignment-resolve


Resolves an assignment by selecting resolution scheme. Could be ‘resolving only’ or ‘resolving by filtering detections’

Base Command

vectra-assignment-resolve

Input

Argument Name Description Required
assignment_id Assignment’s ID. Optional
outcome_id Assignment Outcome’s ID. Optional
note A note to add to this resolution. Optional
detections_filter Do you want to filter detections when resolving this assignment ? [Default is None]. Possible values are: None, Filter Rule. Optional
filter_rule_name Filter rule’s name (when using filter_detections=”Filter Rule”). Optional
detections_list Detection IDs list you want to filter. Optional

Context Output

Path Type Description
Vectra.Assignment.AccountID Number Account ID this assignment is linked to
Vectra.Assignment.AssignedBy String Who lastly assigned this assignment
Vectra.Assignment.AssignedDate String When this assignment was lastly assigned
Vectra.Assignment.AssignedTo String To who this assignment is assigned
Vectra.Assignment.HostID String Host ID this assignment is linked to
Vectra.Assignment.ID Number Assignment ID (unique)
Vectra.Assignment.IsResolved Boolean Is this assignment resolved
Vectra.Assignment.OutcomeCategory String Assignment Outcome category
Vectra.Assignment.OutcomeTitle String Assignment Outcome title
Vectra.Assignment.TriagedDetections String List of Detection that have been triaged with the resolution
Vectra.Assignment.TriagedAs String Name of the triage rule if any
Vectra.Assignment.ResolvedBy String Who resolved this assignment
Vectra.Assignment.ResolvedDate string When this assignment was resolved

vectra-user-describe


Returns a single Vectra User details

Base Command

vectra-user-describe

Input

Argument Name Description Required
id User ID you want to get details on. Optional

Context Output

Path Type Description
Vectra.User.Email String User’s email address
Vectra.User.ID Number User ID (unique)
Vectra.User.Role String User’s role
Vectra.User.Type String User type (‘Local’, ‘SAML’, …)
Vectra.User.Username String Username
Vectra.User.LastLoginDate String User’s last login datetime

vectra-group-list


Returns a list of all groups.

Base Command

vectra-group-list

Input

Argument Name Description Required
group_type Filter by group type. Possible values are: account, host, ip, domain. Optional
account_names Filter by Account Names. Supports comma-separated values.

Note: Only valid when the group_type parameter is set to “account”.
Optional
domains Filter by Domains. Supports comma-separated values.

Note: Only valid when the group_type parameter is set to “domain”.
Optional
host_ids Filter by Host IDs. Supports comma-separated values.

Note: Only valid when the group_type parameter is set to “host”.
Optional
host_names Filter by Host Names. Supports comma-separated values.

Note: Only valid when the group_type parameter is set to “host”.
Optional
importance Filter by group importance. Possible values are: high, medium, low, never_prioritize. Optional
ips Filter by IPs. Supports comma-separated values.

Note: Only valid when the group_type parameter is set to “ip”.
Optional
description Filter by group description. Optional
last_modified_timestamp Return only the groups which have a last modification timestamp equal to or after the given timestamp.

Supported formats: 2 minutes, 2 hours, 2 days, 2 weeks, 2 months, 2 years, yyyy-mm-dd, yyyy-mm-ddTHH:MM:SSZ.

For example: 01 May 2023, 01 Mar 2023 04:45:33, 2023-04-17T14:05:44Z.
Optional
last_modified_by Filters by the user id who made the most recent modification to the group. Optional
group_name Filters by group name. Optional

Context Output

Path Type Description
Vectra.Group.group_id Number ID of the group.
Vectra.Group.name String Name of the group.
Vectra.Group.description String Description of the group.
Vectra.Group.last_modified Date Date when the group was last modified.
Vectra.Group.last_modified_by String Name of the user who last modified the group.
Vectra.Group.type String Type of the group.
Vectra.Group.members.id Number Entity ID of member.
Vectra.Group.members.name String Entity name of member.
Vectra.Group.members.is_key_asset Boolean Indicates key asset.
Vectra.Group.members.url String Entity URL of member.
Vectra.Group.members.uid String Entity UID of member.
Vectra.Group.rules.triage_category String Triage category of rule.
Vectra.Group.rules.id Number ID of the rule.
Vectra.Group.rules.description String Description of the rule.
Vectra.Group.importance String Importance level of the group.
Vectra.Group.cognito_managed Boolean Whether the group is managed by Cognito or not.

Command example

!vectra-group-list group_type=account importance=high

Context Example

{
    "Vectra": {
        "Group": [
            {
                "description": "",
                "group_id": 1,
                "id": 1,
                "last_modified": "2024-07-22T06:44:44Z",
                "last_modified_by": "cds_xsoar",
                "members": [
                    {
                        "uid": "user@lab.test.local"
                    },
                    {
                        "uid": "O365:serviceprincipal_00000000-0000-0000-0000-000000000001"
                    }
                ],
                "name": "AccountNoBlock",
                "type": "account"
            },
            {
                "description": "",
                "group_id": 2,
                "id": 2,
                "last_modified": "2024-07-22T06:44:40Z",
                "last_modified_by": "cds_xsoar",
                "members": [
                    {
                        "uid": "O365:serviceprincipal_00000000-0000-0000-0000-000000000001"
                    }
                ],
                "name": "AccountBlock",
                "type": "account"
            }
        ]
    }
}

Human Readable Output

Groups Table

Group ID Name Group Type Members Last Modified Timestamp
1 AccountNoBlock account user@lab.test.local, O365:serviceprincipal_00000000-0000-0000-0000-000000000001 2024-07-22T06:44:44Z
2 AccountBlock account O365:serviceprincipal_00000000-0000-0000-0000-000000000001 2024-07-22T06:44:40Z

vectra-group-assign


Assign members to the specified group.

Base Command

vectra-group-assign

Input

Argument Name Description Required
group_id Specify Group ID to assign members.

Note: You can get the group_id by executing the "vectra-group-list" command.
Required
members A comma-separated list of member values based on the group type.

Note:
You can get the members by executing the "vectra-group-list" command.
If the group type is host, then the “Host IDs”.
If the group type is account, then “Account Names”.
If the group type is ip, then the list of “IPs”.
If the group type is domain, then the list of “Domains”.
Required

Context Output

Path Type Description
Vectra.Group.group_id Number ID of the group.
Vectra.Group.name String Name of the group.
Vectra.Group.description String Description of the group.
Vectra.Group.last_modified Date Date when the group was last modified.
Vectra.Group.last_modified_by String Name of the user who last modified the group.
Vectra.Group.type String Type of the group.
Vectra.Group.members.id Number Entity ID of member.
Vectra.Group.members.name String Entity name of member.
Vectra.Group.members.is_key_asset Boolean Indicates key asset.
Vectra.Group.members.url String Entity URL of member.
Vectra.Group.members.uid String Entity UID of member.
Vectra.Group.rules.triage_category String Triage category of rule.
Vectra.Group.rules.id Number ID of the rule.
Vectra.Group.rules.description String Description of the rule.

Command example

!vectra-group-assign group_id=3557 members="account_4"

Context Example

{
    "Vectra": {
        "Group": {
        "id": 3,
        "name": "xsoar-account-group-2",
        "last_modified": "2023-09-04T09:22:46Z",
        "last_modified_by": "TEST Client",
        "members": [
            {
                "uid": "account_1"
            },
            {
                "uid": "account_2"
            },
            {
                "uid": "account_3"
            },
            {
                "uid": "account_4"
            }
        ],
        "type": "account",
        "group_id": 3
        }
    }
}

Human Readable Output

Member(s) account_4 have been assigned to the group

Updated group details

Group ID Name Group Type Members Last Modified Timestamp
3 xsoar-account-group-2 account account_1, account_2, account_3, account_4 2023-09-04T09:22:46Z

vectra-group-unassign


Unassign members from the specified group.

Base Command

vectra-group-unassign

Input

Argument Name Description Required
group_id Specify Group ID to unassign members.

Note: You can get the group_id by executing the "vectra-group-list" command.
Required
members A comma-separated list of member values based on the group type.

Note:
You can get the members by executing the "vectra-group-list" command.
If the group type is host, then the “Host IDs”.
If the group type is account, then “Account Names”.
If the group type is ip, then the list of “IPs”.
If the group type is domain, then the list of “Domains”.
Required

Context Output

Path Type Description
Vectra.Group.group_id Number ID of the group.
Vectra.Group.name String Name of the group.
Vectra.Group.description String Description of the group.
Vectra.Group.last_modified Date Date when the group was last modified.
Vectra.Group.last_modified_by String Name of the user who last modified the group.
Vectra.Group.type String Type of the group.
Vectra.Group.members.id Number Entity ID of member.
Vectra.Group.members.name String Entity name of member.
Vectra.Group.members.is_key_asset Boolean Indicates key asset.
Vectra.Group.members.url String Entity URL of member.
Vectra.Group.members.uid String Entity UID of member.
Vectra.Group.rules.triage_category String Triage category of rule.
Vectra.Group.rules.id Number ID of the rule.
Vectra.Group.rules.description String Description of the rule.

Command example

!vectra-group-unassign group_id=5 members="2126"

Context Example

{
    "Vectra": {
        "Group": {
            "id": 2,
            "group_id": 2,
            "type": "host",
            "name": "TEST RENAME",
            "description": "TEST RENAME",
            "last_modified": "2023-09-04T06:27:57Z",
            "last_modified_by": "TEST Client"
        }
    }
}

Human Readable Output

Member(s) 2126 have been unassigned from the group

Updated group details

Group ID Name Group Type Description Last Modified Timestamp
2 TEST RENAME host TEST RENAME 2023-09-04T06:27:57Z

Troubleshooting

Receive Notification on an Incident Fetch Error

The administrator and Cortex XSOAR users on the recipient’s list receive a notification when an integration experiences an incident fetch error. Cortex XSOAR users can select their notification method, such as email, from their user preferences. Refer to Cortex XSOAR 6.13 documentation or Cortex XSOAR 8 Cloud documentation or Cortex XSOAR 8.7 On-prem documentation for more information.

The following are tips for handling issues with mirroring incidents between Vectra and Cortex XSOAR
Issue Recommendation
Mirroring is not working. Open Context Data and search for dbot. Confirm the dbot fields are configured correctly either through the mapper for that specific incident type or using setIncident. Specifically, make sure the integration instance is configured correctly for the mirroring direction (incoming, outgoing, both) - dbotMirrorId, dbotMirrorDirection, dbotMirrorInstance, dbotMirrorTags.
Required fields are not getting sent or not visible in UI. This may be a mapping issue, specifically if you have used a custom mapper make sure you’ve covered all the out of box mapper fields.
Notes from Cortex XSOAR have not been mirrored in Vectra Tag is required for mirroring notes from Cortex XSOAR to Vectra. There might be a reason the note is not tagged as the tag needs to be added manually in Cortex XSOAR.
Click Actions > Tags and add the “note” tag (OR the specific tag name which was set up in the Instance Configuration).

Docker timeout issue for Fetch Incidents

  • If you encounter a timeout error while fetching incidents, you can try adjusting the value of the max_fetch parameter in the instance configuration. Setting it to a lower value, such as 50 can help prevent the timeout issue.

  • Another way to address this issue is to increase the timeout of the Docker container. By default, Docker containers have a timeout of 5 minutes. You can increase this timeout to a higher value, such as 10 minutes, to allow more time for the fetch command to complete. Refer to this XSOAR documentation for more information.

Handling HTTP 429 and 5xx Errors

The commands and fetch incidents mechanism will do up to 3 internal retries with a gap of 15, 30, and 60 seconds (exponentially) between the retries.

Configuration parameters

  • server_fqdn — Vectra Detect FQDN or IP (required)
  • authentication_type — Authentication Type
  • credentials — API Token
  • oauth_credentials — Client ID
  • insecure — Trust any certificate (not secure)
  • use_proxy — Use system proxy settings
  • isFetch — Fetch incidents
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval
  • first_fetch — First fetch timestamp
  • mirror_direction — Mirroring Direction
  • refetch_closed_incidents — Re-Fetch closed incidents via mirroring
  • note_tag — Mirror tag for notes
  • fetch_entity_types — Entity types to fetch
  • tags — Tags
  • detection_category — Detection Category
  • detection_type — Detection Type
  • hosts_fetch_query — Hosts fetch query
  • accounts_fetch_query — Accounts fetch query
  • detections_fetch_query — Detections fetch query
  • max_fetch — Max created incidents per fetch
  • look_back — Advanced: Minutes to look back when fetching
  • fetch_escalated_accounts_and_hosts — Advanced: Fetch escalated Accounts and Hosts
  • create_multiple_incidents_by_timestamp — Re-Fetch Account/Host entity and Detection for new activity/event

Commands (47)

  • vectra-account-add-tags

    Add tags to an Account.

  • vectra-account-del-tags

    Delete tags from an Account.

  • vectra-account-describe

    Returns a single Account details.

  • vectra-account-markall-detections-asclosed

    Mark active detections as closed by providing the ID of the account in the argument.

  • vectra-account-markall-detections-asfixed

    Mark active detections as fixed by providing the ID of the account in the argument.

  • vectra-account-note-add

    Add a note to the account.

  • vectra-account-note-list

    List all notes of the specific account.

  • vectra-account-note-remove

    Remove a note from the account.

  • vectra-account-note-update

    Update a note in the account.

  • vectra-account-tag-list

    Returns a list of tags for a specified account.

  • vectra-assignment-assign

    Assigns an Account/Host entity to a Vectra User for investigation. If an assignment already exists on this entity, it will be reassigned.

  • vectra-assignment-describe

    Returns a single assignment details.

  • vectra-assignment-resolve

    Resolves an assignment by selecting resolution scheme. Could be 'resolving only' or 'resolving by filtering detections'.

  • vectra-detection-add-tags

    Add tags to a Detection.

  • vectra-detection-del-tags

    Delete tags from a Detection.

  • vectra-detection-describe

    Returns a single detection details.

  • vectra-detection-get-pcap

    Returns a Detection's PCAP file (if available).

  • vectra-detection-markasfixed

    Marks/Unmarks a Detection as fixed by providing the Detection ID.

  • vectra-detection-note-add

    Add a note to the detection.

  • vectra-detection-note-list

    List all notes of the specific detection.

  • vectra-detection-note-remove

    Remove a note from the detection.

  • vectra-detection-note-update

    Update a note in the detection.

  • vectra-detection-tag-list

    Returns a list of tags for a specified detection.

  • vectra-detections-mark-asclosed

    Mark detections as closed with provided detection IDs in the argument.

  • vectra-detections-mark-asopen

    Open detections with provided detection IDs in the argument.

  • vectra-group-assign

    Assign members to the specified group.

  • vectra-group-list

    Returns a list of all groups.

  • vectra-group-unassign

    Unassign members from the specified group.

  • vectra-host-add-tags

    Add tags to an Host.

  • vectra-host-del-tags

    Delete tags from an Host.

  • vectra-host-describe

    Returns a single Host details.

  • vectra-host-markall-detections-asclosed

    Mark active detections as closed by providing the ID of the host in the argument.

  • vectra-host-markall-detections-asfixed

    Mark active detections as fixed by providing ID of the host in the argument.

  • vectra-host-note-add

    Add a note to the host.

  • vectra-host-note-list

    List all notes of the specific host.

  • vectra-host-note-remove

    Remove a note from the host.

  • vectra-host-note-update

    Update a note in the host.

  • vectra-host-tag-list

    Returns a list of tags for a specified host.

  • vectra-outcome-create

    Creates a new assignment outcome.

  • vectra-outcome-describe

    Returns a single outcome details.

  • vectra-search-accounts

    Returns a list of Account objects. All search attributes will be cumulative unless you're using the search_query_only one, in that case, only this one will be taken into account.

  • vectra-search-assignments

    Return a list of assignments. By default already resolved assignment are not returned.

  • vectra-search-detections

    Returns a list of Detection objects. All search attributes will be cumulative unless you're using the search_query_only one, in that case, only this one will be taken into account.

  • vectra-search-hosts

    Returns a list of Host objects. All search attributes will be cumulative unless you're using the search_query_only one, in that case, only this one will be taken into account.

  • vectra-search-outcomes

    Returns a list of assignment outcomes.

  • vectra-search-users

    Returns a list of Vectra Users. All search attributes will be cumulative.

  • vectra-user-describe

    Returns a single Vectra User details.

# ## ### #### #####
# Vectra Detect Integration for Cortex XSOAR
#
# Developer Documentation: https://xsoar.pan.dev/docs/welcome
# Code Conventions: https://xsoar.pan.dev/docs/integrations/code-conventions
# Linting: https://xsoar.pan.dev/docs/integrations/linting
# ## ### #### #####

# Python linting disabled example (disable linting on error code E203)
# noqa: E203

# Standard libraries
import json
from copy import deepcopy
from typing import Any
from urllib.parse import parse_qs, urlparse

# Specific libraries
import dateparser  # type: ignore[import]

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

# Disable insecure warnings
urllib3.disable_warnings()

# ####       #### #
# ## CONSTANTS ## #

TOTAL_RETRIES = 4
STATUS_CODE_TO_RETRY = (429, *(status_code for status_code in requests.status_codes._codes if status_code >= 500))  # type: ignore
OK_CODES = (200, 201, 204, 401)
BACKOFF_FACTOR = 7.5  # Sleep for [0s, 15s, 30s, 60s] between retries.
PACK_VERSION = get_pack_version() or "2.1.0"
UTM_PIVOT = f"?pivot=Vectra_AI-XSOAR-{PACK_VERSION}"
DATE_FORMAT: str = "%Y-%m-%dT%H:%M:%S.000Z"
USER_AGENT = f"Vectra_AI-XSOAR-{PACK_VERSION}"
MAX_RESULTS: int = 200
DEFAULT_FIRST_FETCH: str = "7 days"
DEFAULT_FETCH_ENTITY_TYPES: list = ["Hosts", "Accounts"]
DEFAULT_MAX_FETCH: int = 50
TAGS_REGEX = re.compile(r"^[\w:._ -]+$", re.U)

ERRORS = {
    "REQUIRED_ARGUMENT": "Please provide valid value of the '{}'. It is required field.",
    "INVALID_INTEGER_VALUE": "'{}' value must be a non-zero and positive integer value.",
    "POSITIVE_VALUE": 'The value of the "{}" must be greater than or equal to 0',
    "INVALID_MAX_FETCH": '"{}" is an invalid value for Max incidents per fetch. The value must be between 1 to 200.',
    "INVALID_COMMAND_ARG_VALUE": (
        "Invalid '{}' value provided. Please ensure it is one of the values from the following options: {}."
    ),
    "INVALID_SUPPORT_FOR_ARG": 'The argument "{}" must be set to "{}" when providing value for argument "{}".',
    "INVALID_OBJECT": "Failed to parse {} object from response: {}",
    "UNAUTHORIZED_REQUEST": "Status code: {}. Unauthorized request: {}.",
    "GENERAL_AUTH_ERROR": "Status code: {}. Error occurred while creating an authorization token.",
}

ENDPOINTS = {
    "ADD_AND_LIST_ACCOUNT_NOTE_ENDPOINT": "/accounts/{}/notes",
    "ADD_AND_LIST_HOST_NOTE_ENDPOINT": "/hosts/{}/notes",
    "ADD_AND_LIST_DETECTION_NOTE_ENDPOINT": "/detections/{}/notes",
    "UPDATE_AND_REMOVE_ACCOUNT_NOTE_ENDPOINT": "/accounts/{}/notes/{}",
    "UPDATE_AND_REMOVE_HOST_NOTE_ENDPOINT": "/hosts/{}/notes/{}",
    "UPDATE_AND_REMOVE_DETECTION_NOTE_ENDPOINT": "/detections/{}/notes/{}",
}

OUTPUT_PREFIXES = {
    "ACCOUNT_NOTES": "Vectra.Account.Notes",
    "HOST_NOTES": "Vectra.Host.Notes",
    "DETECTION_NOTES": "Vectra.Detection.Notes",
}

NOTE_OUTPUT_KEY_FIELD = "note_id"

API_VERSION_URL = "/api/v2.5"

API_ENDPOINT_OAUTH_TOKEN = "/oauth2/token"

API_ENDPOINT_ACCOUNTS = "/accounts"
API_ENDPOINT_ASSIGNMENT = "/assignments"
API_ENDPOINT_OUTCOMES = "/assignment_outcomes"
API_ENDPOINT_DETECTIONS = "/detections"
API_ENDPOINT_HOSTS = "/hosts"
API_ENDPOINT_USERS = "/users"
API_ENDPOINT_GROUPS = "/groups"

API_SEARCH_ENDPOINT_ACCOUNTS = "/search/accounts"
API_SEARCH_ENDPOINT_DETECTIONS = "/search/detections"
API_SEARCH_ENDPOINT_HOSTS = "/search/hosts"

API_TAGGING = "/tagging"

UI_ACCOUNTS = "/accounts"
UI_DETECTIONS = "/detections"
UI_HOSTS = "/hosts"

DEFAULT_ORDERING = {
    "accounts": {"ordering": "last_detection_timestamp"},
    "detections": {"ordering": "last_timestamp"},
    "hosts": {"ordering": "last_detection_timestamp"},
}
DEFAULT_STATE = {"state": "active", "resolved": "false"}

ENTITY_TYPES = ("Accounts", "Hosts", "Detections")

OUTCOME_CATEGORIES = {
    "benign_true_positive": "Benign True Positive",
    "malicious_true_positive": "Malicious True Positive",
    "false_positive": "False Positive",
}
ASSIGNMENT_ENTITY_TYPES = ("account", "host")

BACK_IN_TIME_SEARCH_IN_MINUTES = "0"

VALID_GROUP_TYPE = ["account", "host", "ip", "domain"]
VALID_IMPORTANCE_VALUE = ["high", "medium", "low", "never_prioritize"]
VALID_CLOSE_REASON = ["benign", "remediated"]

MAX_MIRRORING_LIMIT = 5000
ENTITY_TYPES_FOR_MIRRORING = ["host", "account"]
EMPTY_ASSIGNMENT = [
    {
        "id": "",
        "date_assigned": "",
        "date_resolved": "",
        "assigned_to": {"username": ""},
        "resolved_by": {"username": ""},
        "assigned_by": {"username": ""},
        "outcome": {"title": ""},
    }
]
MIRROR_DIRECTION = {"Incoming": "In", "Outgoing": "Out", "Incoming And Outgoing": "Both"}
EARLY_EXPIRY_TIME = 300  # (in seconds)

# ####     #### #
# ## GLOBALS ## #
global_UI_URL: str | None = None


# ####          #### #
# ## CLIENT CLASS ## #
class Client(BaseClient):
    """Client class to interact with the service API

    This Client implements API calls, and does not contain any XSOAR logic.
    Should only do requests and return data.
    It inherits from BaseClient defined in CommonServer Python.
    Most calls use http_request() that handles proxy, SSL verification, etc.
    Supports both API token and OAuth 2.0 authentication methods.
    """

    def __init__(
        self,
        base_url: str,
        verify: bool,
        proxy: bool,
        use_oauth: bool = False,
        api_token: str = None,
        client_id: str = None,
        client_secret: str = None,
        headers: dict = None,
    ):
        """
        Initializes the Client instance with authentication support.

        Args:
            base_url (str): The base URL for the API.
            verify (bool): Whether to verify SSL certificates.
            proxy (bool): Whether to use proxy.
            use_oauth (bool): Whether to use OAuth 2.0 authentication (True) or API token (False).
            api_token (str): API token for token-based authentication.
            client_id (str): Client ID for OAuth authentication.
            client_secret (str): Client secret for OAuth authentication.
            headers (dict): Additional headers to include in requests.
        """
        super().__init__(base_url=base_url, verify=verify, proxy=proxy, headers=headers)
        self.use_oauth = use_oauth
        self.api_token = api_token
        self.client_id = client_id
        self.client_secret = client_secret

    def _get_auth_headers(self) -> dict:
        """
        Get authentication headers based on the configured auth type.

        Returns:
            dict: Headers with appropriate authentication.
        """
        headers = {"User-Agent": USER_AGENT}

        if self.use_oauth:
            token = self._generate_tokens()
            headers["Authorization"] = f"Bearer {token}"
        elif not self.use_oauth and self.api_token:
            headers["Authorization"] = f"token {self.api_token}"

        return headers

    def _generate_tokens(self, use_existing_token: bool = True) -> str:
        """
        Get an OAuth access token that was previously created if it is still valid,
        else, generate a new authorization token from the client id and client secret.

        Args:
            use_existing_token: Use existing token if it is still valid.

        Returns:
            str: The access token.
        """
        integration_context: dict = get_integration_context()
        previous_token: dict = integration_context.get("oauth_token", {})

        # Check if there is existing valid authorization token.
        if (
            previous_token.get("access_token")
            and use_existing_token
            and previous_token.get("expire") > datetime.now(timezone.utc).timestamp()  # type: ignore
        ):
            demisto.debug("Got OAuth access token from the integration context.")
            return previous_token.get("access_token")  # type: ignore

        demisto.debug("Trying to generate a new OAuth access token.")
        payload = "grant_type=client_credentials"
        auth = requests.auth.HTTPBasicAuth(self.client_id, self.client_secret)  # type: ignore
        headers = {"User-Agent": USER_AGENT, "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}

        res = self._http_request(
            method="POST",
            url_suffix=API_ENDPOINT_OAUTH_TOKEN,
            headers=headers,
            data=payload,
            auth=auth,
            backoff_factor=BACKOFF_FACTOR,
            status_list_to_retry=STATUS_CODE_TO_RETRY,
            resp_type="response",
            ok_codes=OK_CODES,
            raise_on_status=True,
        )
        res_status_code = res.status_code
        if res_status_code in [401]:
            raise DemistoException(ERRORS["GENERAL_AUTH_ERROR"].format(res_status_code))

        try:
            res_json = res.json()
        except ValueError as exception:
            raise DemistoException(ERRORS["INVALID_OBJECT"].format("json", res.content), exception)

        if res_json.get("access_token"):
            # Calculate expiry time from expires_in (seconds) with early expiry buffer
            expires_in_seconds = res_json.get("expires_in", 21600)  # Default to 6 hours
            expiry_time = datetime.now(timezone.utc).timestamp() + expires_in_seconds - EARLY_EXPIRY_TIME  # 5 min buffer
            demisto.debug(
                "Setting the expiry time of the OAuth access token to " + timestamp_to_datestring(expiry_time * 1000, is_utc=True)
            )
            new_token = {"access_token": res_json.get("access_token"), "expire": expiry_time}
            integration_context.update({"oauth_token": new_token})
            set_integration_context(integration_context)

            return res_json.get("access_token")

        raise DemistoException("Failed to generate OAuth access token - no access_token in response")

    def http_request(
        self,
        method,
        url_suffix,
        params=None,
        status_list_to_retry=STATUS_CODE_TO_RETRY,
        backoff_factor=BACKOFF_FACTOR,
        retries=TOTAL_RETRIES,
        internal_retries=3,
        json_data=None,
        resp_type="json",
        **kwargs,
    ):
        """
        Method to override private _http_request of BaseClient to handle specific status code.

        :type method: ``str``
        :param method: The HTTP method, for example: GET, POST, and so on.

        :type url_suffix: ``str``
        :param url_suffix: The API endpoint.

        :type params: ``dict``
        :param params: URL parameters to specify the query.

        :type status_list_to_retry: ``iterable``
        :param status_list_to_retry: A set of integer HTTP status codes that we should force a retry on.

        :type backoff_factor ``float``
        :param backoff_factor: A backoff factor to apply between attempts

        :type retries: ``int``
        :param retries: How many retries should be made in case of a failure.

        :type internal_retries: ``int``
        :param internal_retries: How many retries should be made in case of an auth failure.

        :type json_data: ``dict``
        :param json_data: Json data to submit with the http request.

        :type resp_type: ``str``
        :param resp_type: Response type.

        :return: Response dict or response object.
        :rtype: ``Optional[dict]`` or ``requests.Response``
        """
        demisto.debug(
            "Requesting Vectra Detect with method: "
            f"{method}, url_suffix: {url_suffix}, params: {params} and json_data: {json_data}"
        )

        # Get authentication headers dynamically
        headers = self._get_auth_headers()
        resp = self._http_request(
            method=method,
            url_suffix=url_suffix,
            headers=headers,
            params=params,
            json_data=json_data,
            retries=retries,
            status_list_to_retry=status_list_to_retry,
            backoff_factor=backoff_factor,
            raise_on_status=True,
            resp_type="response",
            ok_codes=OK_CODES,
            **kwargs,
        )
        resp_status_code = resp.status_code

        # If authentication failure happens for OAuth.
        if resp_status_code in [401]:
            if self.use_oauth:
                demisto.debug("Handling status code 401 by generating a new OAuth token.")
                if internal_retries > 0:
                    self._generate_tokens(use_existing_token=False)
                    internal_retries = internal_retries - 1
                    return self.http_request(
                        method=method,
                        url_suffix=url_suffix,
                        params=params,
                        status_list_to_retry=status_list_to_retry,
                        backoff_factor=backoff_factor,
                        retries=retries,
                        internal_retries=internal_retries,
                        json_data=json_data,
                        resp_type=resp_type,
                        **kwargs,
                    )
                try:
                    err_msg = ERRORS["UNAUTHORIZED_REQUEST"].format(resp_status_code, str(resp.json()))
                except ValueError:
                    err_msg = ERRORS["UNAUTHORIZED_REQUEST"].format(resp_status_code, str(resp))
                raise DemistoException(err_msg)
            else:
                raise DemistoException(ERRORS["UNAUTHORIZED_REQUEST"].format(resp_status_code, str(resp.json())))

        try:
            result = None
            if resp_type == "json":
                result = resp.json()
            if resp_type == "content":
                result = resp.content()
            if resp_type == "response":
                result = resp
            if resp_type == "text":
                result = resp.text
        except ValueError as exception:
            raise DemistoException(
                ERRORS["INVALID_OBJECT"].format(resp_type, resp.content),  # type: ignore[str-bytes-safe]
                exception,
                resp,
            )
        return result

    def search_detections(
        self,
        min_id=None,
        max_id=None,
        min_threat=None,
        max_threat=None,
        min_certainty=None,
        max_certainty=None,
        last_timestamp=None,
        state: str = None,  # type: ignore
        search_query: str = None,
        search_query_only: str = None,  # type: ignore
        max_results=None,  # type: ignore
        **kwargs,
    ) -> dict[str, Any]:
        """
        Gets Detections using the 'detections' API endpoint

        :return: dict containing all Detections details
        :rtype: ``Dict[str, Any]``
        """

        # Default params
        demisto.debug("Forcing 'page', 'order_field' and 'page_size' query arguments")
        query_params: dict[str, Any] = {"page": 1, "order_field": "last_timestamp"}
        query_params["page_size"] = sanitize_max_results(max_results)

        params: dict[str, Any] = {}

        if search_query_only:
            # Specific search query used
            query_params["query_string"] = search_query_only
        else:
            # Test min_id / max_id
            validate_min_max("min_id", min_id, "max_id", max_id)
            if min_id:
                params["min_id"] = min_id
            if max_id:
                params["max_id"] = max_id

            # Test min_threat / max_threat
            validate_min_max("min_threat", min_threat, "max_threat", max_threat)
            if min_threat:
                params["min_threat"] = min_threat
            if max_threat:
                params["max_threat"] = max_threat

            # Test min_certainty / max_certainty
            validate_min_max("min_certainty", min_certainty, "max_certainty", max_certainty)
            if min_certainty:
                params["min_certainty"] = min_certainty
            if max_certainty:
                params["max_certainty"] = max_certainty

            # Last timestamp
            if last_timestamp:
                params["last_timestamp"] = last_timestamp

            # State
            if state:
                params["state"] = state
            else:
                params["state"] = DEFAULT_STATE["state"]

            # Build search query
            query_params["query_string"] = build_search_query("detection", params)

            # Adding additional search query
            if search_query:
                query_params["query_string"] += f" AND {search_query}"

        demisto.debug(f"Search query : '{query_params['query_string']}'")

        # Execute request
        demisto.debug("Executing API request")
        return self.http_request(method="GET", params=query_params, url_suffix=f"{API_SEARCH_ENDPOINT_DETECTIONS}")

    def list_detections_by_host_id(
        self, host_id: str, state: str = "active", detection_category: str = "", detection_type: str = ""
    ) -> dict:
        """
        Get Detections by Host ID.

        :param host_id: Host ID.
        :param state: The state of the detections.
        :param detection_category: Detection Category.
        :param detection_type: Detection Type.

        :return: List of Detections.
        :rtype: ``Dict``
        """
        params = assign_params(detection_category=detection_category, detection_type=detection_type, state=state, host_id=host_id)

        return self.http_request(method="GET", url_suffix=f"{API_ENDPOINT_DETECTIONS}", params=params)

    def search_accounts(
        self,
        min_id=None,
        max_id=None,
        min_threat=None,
        max_threat=None,
        min_certainty=None,
        max_certainty=None,
        last_timestamp=None,
        state: str = None,  # type: ignore
        search_query: str = None,
        search_query_only: str = None,  # type: ignore
        page: int = None,
        max_results=None,  # type: ignore
        order_field: str = "last_detection_timestamp",  # type: ignore
        doc_modified_time=None,
        **kwargs,
    ) -> dict[str, Any]:
        """
        Gets Accounts using the 'Search Accounts' API endpoint.

        :return: dict containing all Accounts details
        :rtype: ``Dict[str, Any]``
        """

        # Default params
        demisto.debug("Forcing 'page', 'order_field' and 'page_size' query arguments")
        query_params: dict[str, Any] = {"page": page if page else 1, "order_field": order_field}
        query_params["page_size"] = sanitize_max_results(max_results)

        params: dict[str, Any] = {}

        if search_query_only:
            # Specific search query used
            query_params["query_string"] = search_query_only
        else:
            # Test min_id / max_id
            validate_min_max("min_id", min_id, "max_id", max_id)
            if min_id:
                params["min_id"] = min_id
            if max_id:
                params["max_id"] = max_id

            # Test min_threat / max_threat
            validate_min_max("min_threat", min_threat, "max_threat", max_threat)
            if min_threat:
                params["min_threat"] = min_threat
            if max_threat:
                params["max_threat"] = max_threat

            # Test min_certainty / max_certainty
            validate_min_max("min_certainty", min_certainty, "max_certainty", max_certainty)
            if min_certainty:
                params["min_certainty"] = min_certainty
            if max_certainty:
                params["max_certainty"] = max_certainty

            # Last timestamp
            if last_timestamp:
                params["last_timestamp"] = last_timestamp

            if doc_modified_time:
                params["doc_modified_time"] = doc_modified_time

            # State
            if state:
                params["state"] = state
            else:
                params["state"] = DEFAULT_STATE["state"]

            # Build search query
            query_params["query_string"] = build_search_query("account", params)

            # Adding additional search query
            if search_query:
                query_params["query_string"] += f" AND {search_query}"

        demisto.debug(f"Search query : '{query_params['query_string']}'")

        # Execute request
        demisto.debug("Executing API request")
        return self.http_request(method="GET", params=query_params, url_suffix=f"{API_SEARCH_ENDPOINT_ACCOUNTS}")

    def search_hosts(
        self,
        min_id=None,
        max_id=None,
        min_threat=None,
        max_threat=None,
        min_certainty=None,
        max_certainty=None,
        last_timestamp=None,
        state: str = None,  # type: ignore
        search_query: str = None,
        search_query_only: str = None,  # type: ignore
        page: int = None,
        max_results=None,  # type: ignore
        order_field: str = "last_detection_timestamp",  # type: ignore
        doc_modified_time=None,
        **kwargs,
    ) -> dict[str, Any]:
        """
        Gets Hosts using the 'hosts' API endpoint.

        :return: dict containing all Hosts details.
        :rtype: ``Dict[str, Any]``
        """

        # Default params
        demisto.debug("Forcing 'page', 'order_field' and 'page_size' query arguments")
        query_params: dict[str, Any] = {"page": page if page else 1, "order_field": order_field}
        query_params["page_size"] = sanitize_max_results(max_results)

        params: dict[str, Any] = {}

        if search_query_only:
            # Specific search query used
            query_params["query_string"] = search_query_only
        else:
            # Test min_id / max_id
            validate_min_max("min_id", min_id, "max_id", max_id)
            if min_id:
                params["min_id"] = min_id
            if max_id:
                params["max_id"] = max_id

            # Test min_threat / max_threat
            validate_min_max("min_threat", min_threat, "max_threat", max_threat)
            if min_threat:
                params["min_threat"] = min_threat
            if max_threat:
                params["max_threat"] = max_threat

            # Test min_certainty / max_certainty
            validate_min_max("min_certainty", min_certainty, "max_certainty", max_certainty)
            if min_certainty:
                params["min_certainty"] = min_certainty
            if max_certainty:
                params["max_certainty"] = max_certainty

            # Last timestamp
            if last_timestamp:
                params["last_timestamp"] = last_timestamp

            if doc_modified_time:
                params["doc_modified_time"] = doc_modified_time

            # State
            if state:
                params["state"] = state
            else:
                params["state"] = DEFAULT_STATE["state"]

            # Build search query
            query_params["query_string"] = build_search_query("host", params)

            # Adding additional search query
            if search_query:
                query_params["query_string"] += f" AND {search_query}"

        demisto.debug(f"Search query : '{query_params['query_string']}'")

        # Execute request
        return self.http_request(method="GET", params=query_params, url_suffix=f"{API_SEARCH_ENDPOINT_HOSTS}")

    def search_assignments(
        self, id=None, account_ids=None, host_ids=None, assignee_ids=None, outcome_ids=None, resolved=None
    ) -> dict[str, Any]:
        """
        Gets Assignments using the 'assignment' API endpoint.

        :return: dict containing all Assignments details.
        :rtype: ``Dict[str, Any]``
        """

        # Default params
        # Assignment endpoint doesn't support pagination
        query_params: dict[str, Any] = {}

        url_addon = f"/{id}" if id else ""

        # If id is specified, do not use other params
        if not id:
            if account_ids and host_ids:
                raise VectraException("Cannot use 'account_ids' and 'host_ids' at the same time")

            # Test Account IDs
            account_ids_set = sanitize_str_ids_list_to_set(account_ids)
            if account_ids_set is not None:
                query_params["accounts"] = account_ids_set

            # Test Host IDs
            host_ids_set = sanitize_str_ids_list_to_set(host_ids)
            if host_ids_set is not None:
                query_params["hosts"] = host_ids_set

            # Test Assignee IDs
            assignee_ids_set = sanitize_str_ids_list_to_set(assignee_ids)
            if assignee_ids_set is not None:
                query_params["assignees"] = assignee_ids_set

            # Test Outcome IDs
            outcome_ids_set = sanitize_str_ids_list_to_set(outcome_ids)
            if outcome_ids_set is not None:
                query_params["resolution"] = outcome_ids_set

            # Resolved
            if resolved:
                query_params["resolved"] = resolved
            else:
                query_params["resolved"] = DEFAULT_STATE["resolved"]

        # Execute request
        return self.http_request(method="GET", params=query_params, url_suffix=f"{API_ENDPOINT_ASSIGNMENT}{url_addon}")

    def search_outcomes(self, id=None, max_results=None) -> dict[str, Any]:
        """
        Gets Assignment outcomes using the 'assignment_outcomes' API endpoint.

        :return: dict containing all Outcomes details.
        :rtype: ``Dict[str, Any]``
        """
        # Default params
        demisto.debug("Forcing 'page' and 'page_size' query arguments")
        query_params: dict[str, Any] = {"page": 1}
        query_params["page_size"] = sanitize_max_results(max_results)

        url_addon = f"/{id}" if id else ""

        # Execute request
        return self.http_request(method="GET", params=query_params, url_suffix=f"{API_ENDPOINT_OUTCOMES}{url_addon}")

    def search_users(self, id=None, last_login_datetime=None, role=None, type=None, username=None) -> dict[str, Any]:
        """
        Gets Vectra Users using the 'assignment_outcomes' API endpoint.

        :return: dict containing all User details.
        :rtype: ``Dict[str, Any]``
        """
        # Default params
        # Users endpoint doesn't support pagination
        query_params: dict[str, Any] = {}

        url_addon = f"/{id}" if id else ""

        # If id is specified, do not use other params
        if not id:
            # Test user name
            if username:
                query_params["username"] = username

            # Test user role
            if role:
                query_params["role"] = role

            # Test user type
            if type:
                query_params["account_type"] = type

            # Test last login datetime
            if last_login_datetime and convert_date(last_login_datetime) is not None:
                query_params["last_login_gte"] = last_login_datetime

        # Execute request
        return self.http_request(method="GET", params=query_params, url_suffix=f"{API_ENDPOINT_USERS}{url_addon}")

    def get_pcap_by_detection_id(self, id: str):
        """
        Gets a single detection PCAP file using the detection endpoint

        - params:
            - id: The Detection ID
        - returns:
            PCAP file if available
        """

        # Execute request
        return self.http_request(method="GET", url_suffix=f"{API_ENDPOINT_DETECTIONS}/{id}/pcap", resp_type="response")

    def markasfixed_by_detection_id(self, id: str, fixed: bool):
        """
        Mark/Unmark a single detection as fixed

        - params:
            - id: Vectra Detection ID
            - fixed: Targeted state
        - returns:
            Vectra API call result (unused)
        """

        json_payload = {"detectionIdList": [id], "mark_as_fixed": "true" if fixed else "false"}

        # Execute request
        return self.http_request(method="PATCH", url_suffix=API_ENDPOINT_DETECTIONS, json_data=json_payload)

    def add_tags(self, id: str, type: str, tags: list[str]):
        """
        Adds tags from Vectra entity

        - params:
            id: The entity ID
            type: The entity type
            tags: Tags list
        - returns
            Vectra API call result (unused)
        """

        # Must be done in two steps
        # 1 - get current tags
        # 2 - merge list and apply

        # Execute get request
        api_response = self.http_request(method="GET", url_suffix=f"{API_TAGGING}/{type}/{id}")

        current_tags: list[str] = api_response.get("tags", [])

        json_payload = {"tags": list(set(current_tags).union(set(tags)))}

        # Execute request
        return self.http_request(method="PATCH", url_suffix=f"{API_TAGGING}/{type}/{id}", json_data=json_payload)

    def del_tags(self, id: str, type: str, tags: list[str]):
        """
        Deletes tags from Vectra entity

        - params:
            id: The entity ID
            type: The entity type
            tags: Tags list
        - returns
            Vectra API call result (unused)
        """

        # Must be done in two steps
        # 1 - get current tags
        # 2 - merge list and apply

        # Execute get request
        api_response = self.http_request(method="GET", url_suffix=f"{API_TAGGING}/{type}/{id}")

        current_tags = api_response.get("tags", [])

        json_payload = {"tags": list(set(current_tags).difference(set(tags)))}

        # Execute request
        return self.http_request(method="PATCH", url_suffix=f"{API_TAGGING}/{type}/{id}", json_data=json_payload)

    def create_outcome(self, category: str, title: str):
        """
        Creates a new Outcome

        - params:
            - category: The Outcome category (one of "BTP,MTP,FP" in human readable format)
            - title: A custom title for this new outcome
        - returns:
            Vectra API call result
        """
        raw_category = convert_outcome_category_text2raw(category)
        if raw_category is None:
            raise ValueError('"category" value is invalid')
        raw_title = title.strip()
        if raw_title == "":
            raise ValueError('"title" cannot be empty')

        json_payload = {"title": raw_title, "category": raw_category}

        # Execute request
        return self.http_request(method="POST", url_suffix=API_ENDPOINT_OUTCOMES, json_data=json_payload)

    def update_assignment(
        self,
        assignee_id: str,
        assignment_id: str = None,  # type: ignore
        account_id: str = None,
        host_id: str = None,
    ):  # type: ignore
        """
        Creates or updates an assignment

        - params:
            - assignee_id: The Vectra User ID who want to assign to
            - assignment_id: The existing assignment ID associated with the targeted Entity, if there is any
            - assignee_id: The Vectra User ID who want to assign to
            - account_id: The Account ID
            - host_id: The Host ID
        - returns:
            Vectra API call result
        """
        # Test Assignee ID
        try:
            validate_argument("min_id", assignee_id)
        except ValueError:
            raise ValueError('"assignee_id" value is invalid')

        json_payload = {
            "assign_to_user_id": assignee_id,
        }

        if assignment_id:  # Reassign an existing assignment
            # Test Assignment ID
            try:
                validate_argument("min_id", assignment_id)
            except ValueError:
                raise ValueError('"assignment_id" value is invalid')

            url_addon = f"/{assignment_id}"

            return self.http_request(method="PUT", url_suffix=f"{API_ENDPOINT_ASSIGNMENT}{url_addon}", json_data=json_payload)
        elif account_id:
            # Test Entity ID
            try:
                validate_argument("min_id", account_id)
            except ValueError:
                raise ValueError('"account_id" value is invalid')

            json_payload.update({"assign_account_id": account_id})

            # Execute request
            return self.http_request(method="POST", url_suffix=API_ENDPOINT_ASSIGNMENT, json_data=json_payload)
        elif host_id:
            # Test Entity ID
            try:
                validate_argument("min_id", host_id)
            except ValueError:
                raise ValueError('"host_id" value is invalid')

            json_payload.update({"assign_host_id": host_id})

            # Execute request
            return self.http_request(method="POST", url_suffix=API_ENDPOINT_ASSIGNMENT, json_data=json_payload)
        else:
            raise ValueError('Either "assignment_id" or "account_id" or "host_id" must be specified.')

    def resolve_assignment(
        self,
        assignment_id: str,
        outcome_id: str,
        note: str = None,  # type: ignore
        rule_name: str = None,
        detections_list: str = None,
    ):  # type: ignore
        """
        Creates or updates an assignment

        - params:
            - assignee_id: The Vectra User ID who want to assign to
            - assignment_id: The existing assignment ID associated with the targeted Entity, if there is any
            - assignee_id: The Vectra User ID who want to assign to
            - account_id: The Account ID
            - host_id: The Host ID
        - returns:
            Vectra API call result
        """
        # Test assignment ID
        try:
            validate_argument("min_id", assignment_id)
        except ValueError:
            raise ValueError('"assignment_id" value is invalid')

        # Test outcome ID
        try:
            validate_argument("min_id", outcome_id)
        except ValueError:
            raise ValueError('"outcome_id" value is invalid')

        json_payload: dict[str, Any] = {
            "outcome": outcome_id,
            "note": note,
        }

        if rule_name:
            detection_ids_set = sanitize_str_ids_list_to_set(detections_list)
            if detection_ids_set is None:
                raise ValueError('"detections_list" value is invalid')

            json_payload.update({"triage_as": rule_name, "detection_ids": list(detection_ids_set)})

        # Execute request
        return self.http_request(
            method="PUT", url_suffix=f"{API_ENDPOINT_ASSIGNMENT}/{assignment_id}/resolve", json_data=json_payload
        )

    def delete_assignment(self, assignment_id: str = ""):  # type: ignore
        """
        Delete the assignment.

        - params:
            - assignment_id: The existing assignment ID associated with the targeted Entity.
        - returns:
            Vectra API call result.
        """
        if assignment_id:
            url_addon = f"/{assignment_id}"
            return self.http_request(method="DELETE", url_suffix=f"{API_ENDPOINT_ASSIGNMENT}{url_addon}", resp_type="response")
        return None

    def markasfixed_by_detection_ids(self, ids_list: list):
        """
        Mark a list of detections as fixed.

        - params:
            - ids_list: Vectra Detection IDs list.
        - returns:
            Vectra API call result.
        """

        json_payload = {"detectionIdList": ids_list, "mark_as_fixed": "true"}

        # Execute request
        return self.http_request(method="PATCH", url_suffix=API_ENDPOINT_DETECTIONS, json_data=json_payload)

    def close_detections_by_ids(self, ids_list: list, reason: str):
        """
        Close a list of detections with a specified reason.

        - params:
            - ids_list: Vectra Detection IDs list.
            - reason: The close reason (benign or remediated).
        - returns:
            Vectra API call result.
        """

        json_payload = {"detectionIdList": ids_list, "reason": reason}

        # Execute request
        return self.http_request(method="PATCH", url_suffix=f"{API_ENDPOINT_DETECTIONS}/close", json_data=json_payload)

    def open_detections_by_ids(self, ids_list: list):
        """
        Open a detections by ID

        - params:
            - ids_list: Vectra Detection IDs list
        - returns:
            Vectra API call result
        """

        json_payload = {"detectionIdList": ids_list}

        # Execute request
        return self.http_request(method="PATCH", url_suffix=f"{API_ENDPOINT_DETECTIONS}/open", json_data=json_payload)

    def list_tags_request(self, entity_id: int = None, entity_type: str = None) -> dict:  # type: ignore
        """
        List tags for the specified entity.

        Args:
            entity_id (int): The ID of the Account/Host/Detection to list tags.
            entity_type (str): The type of the entity.

        Returns:
            Dict: Response from the API containing the tags.
        """
        res = self.http_request(method="GET", url_suffix=f"{API_TAGGING}/{entity_type}/{entity_id}")
        return res

    def add_note_request(self, entity_id: int = None, entity_type: str = None, note: str = None) -> dict:  # type: ignore
        """
        Add a note to an Account, Host or Detection.

        Args:
            entity_id (int): The ID of the Account, Host or Detection to add the note to.
            entity_type (str): The type Account, Host or Detection.
            note (str): The note to add.

        Returns:
            Dict: Response from the API containing the added note.
        """
        data = {"note": note}
        notes = self.http_request(
            method="POST",
            json_data=data,
            url_suffix=ENDPOINTS[f"ADD_AND_LIST_{entity_type.upper()}_NOTE_ENDPOINT"].format(  # type: ignore[union-attr]
                entity_id
            ),  # type: ignore
        )
        return notes

    def update_note_request(
        self,
        entity_id: int = None,
        entity_type: str = None,
        note: str = None,  # type: ignore
        note_id: int = None,
    ) -> dict:  # type: ignore
        """
        Updates the note of an Account, Host or Detection.

        Args:
            entity_id (int): The ID of the Account, Host or Detection to update the note for.
            entity_type (str): The type Account, Host or Detection.
            note (str): The updated note for the Account, Host or Detection.
            note_id (int): The ID of the note to be updated.

        Returns:
            Dict: Response from the API containing the updated note details.
        """
        data = {"note": note}
        notes = self.http_request(
            method="PATCH",
            json_data=data,
            url_suffix=ENDPOINTS[f"UPDATE_AND_REMOVE_{entity_type.upper()}_NOTE_ENDPOINT"].format(  # type: ignore[union-attr]
                entity_id, note_id
            ),
        )

        return notes

    def remove_note_request(
        self,
        entity_type: str = None,
        entity_id: int = None,  # type: ignore
        note_id: str = None,
    ) -> requests.Response:  # type: ignore
        """
        Remove a note from an Account, Host or Detection.

        Args:
            entity_id (int): The ID of the Account, Host or Detection to remove the note from .
            entity_type (str): The type Account, Host or Detection.
            note_id (int): The Id of the note to be removed.

        Returns:
            requests.Response: Response from the API containing the added note.
        """
        res = self.http_request(
            method="DELETE",
            resp_type="response",
            url_suffix=ENDPOINTS[f"UPDATE_AND_REMOVE_{entity_type.upper()}_NOTE_ENDPOINT"].format(  # type: ignore[union-attr]
                entity_id, note_id
            ),
        )
        return res

    def list_note_request(self, entity_id: int = None, entity_type: str = None) -> dict:  # type: ignore
        """
        List Account/Host/Detection notes.

        Args:
            entity_id (int): The ID of the Account/Host/Detection to add the note to.
            entity_type (str): The type Account, Host or Detection.

        Returns:
            Dict: Response from the API.
        """
        notes = self.http_request(
            method="GET",
            url_suffix=ENDPOINTS[f"ADD_AND_LIST_{entity_type.upper()}_NOTE_ENDPOINT"].format(  # type: ignore[union-attr]
                entity_id
            ),
        )
        return notes

    def get_account_by_account_id(self, account_id: str | None = None):
        """
        Get Account by Account ID.

        - params:
            - account_id: The Account ID
        - returns:
            Vectra API call result.
        """
        return self.http_request(method="GET", url_suffix=f"{API_ENDPOINT_ACCOUNTS}/{account_id}")

    def get_host_by_host_id(self, host_id: str | None = None):
        """
        Get Host by Host ID.

        - params:
            - host_id: The Host ID
        - returns:
            Vectra API call result.
        """
        return self.http_request(method="GET", url_suffix=f"{API_ENDPOINT_HOSTS}/{host_id}")

    def get_group_request(self, group_id: int = None) -> dict:  # type: ignore
        """Get group by ID.

        Args:
            group_id (int): The ID of the group to retrieve.

        Returns:
            Dict: Response from the API containing the group information.
        """
        return self.http_request(method="GET", url_suffix=f"{API_ENDPOINT_GROUPS}/{group_id}")

    def update_group_members_request(self, group_id: int = None, members: list = None) -> dict:  # type: ignore
        """Update members in group.

        Args:
            group_id (int): The ID of the group to retrieve.
            members (List): The member list.

        Returns:
            Dict: Response from the API containing the group information.
        """
        body = {"members": members}
        return self.http_request(method="PATCH", url_suffix=f"{API_ENDPOINT_GROUPS}/{group_id}", json_data=body)

    def list_assignments_request(
        self, accounts: str | None = None, hosts: str | None = None, page_size: int | None = None
    ) -> dict:
        """
        Get Assignments by Accounts and Hosts.

        - params:
            - accounts: The Accounts IDs
            - hosts: The Host IDs
            - page_size: Total number of Assignments
        - returns:
            Vectra API call result.
        """
        query_params = {}
        if page_size:
            query_params = {"page_size": page_size}
        if accounts:
            query_params["accounts"] = accounts  # type: ignore
        if hosts:
            query_params["hosts"] = hosts  # type: ignore
        return self.http_request(method="GET", url_suffix=f"{API_ENDPOINT_ASSIGNMENT}", params=query_params)

    def list_entity_tags_request(self, entity_id: str | None = None, entity_type: str | None = None):
        """
        Get Entity Tags by Entity ID and Entity Type.

        - params:
            - entity_id: The Entity ID
            - entity_type: The Entity Type
        - returns:
            Vectra API call result.
        """
        return self.http_request(method="GET", url_suffix=f"{API_TAGGING}/{entity_type}/{entity_id}")

    def update_entity_tags_request(
        self, entity_id: str | None = None, entity_type: str | None = None, tag_list: list | None = None
    ):
        """
        Update Entity Tags by Entity ID and Entity Type.

        - params:
            - entity_id: The Entity ID
            - entity_type: The Entity Type
            - tag_list: The List of Tags
        - returns:
            Vectra API call result.
        """
        tags = {"tags": tag_list}
        return self.http_request(method="PATCH", url_suffix=f"{API_TAGGING}/{entity_type}/{entity_id}", json_data=tags)

    def list_group_request(
        self,
        group_type: str = "",
        account_names: list[str] = [],
        domains: list[str] = [],
        host_ids: list[str] = [],
        host_names: list[str] = [],
        importance: str = "",
        ips: list[str] = [],
        description: str = "",
        last_modified_timestamp: datetime | None = None,
        last_modified_by: str = "",
        group_name: str = "",
    ):
        """
        List groups as per the specified parameters.

        Args:
            group_type (str): Filter by group type.
            account_names (List[str]): Filter groups associated with accounts.
            domains (List[str]): Filter groups associated with domains.
            host_ids (List[str]): Filter groups associated with hosts.
            host_names (List[str]): Filter groups associated with hosts.
            importance (str): User defined group importance.
            ips (List[str]): Filter groups associated with ips.
            description (List[str]): Filter by group description.
            last_modified_timestamp (Optional[datetime]):
                Filters for all groups modified on or after the given timestamp (GTE).
            last_modified_by (str): Filters groups by the user id who made the most recent modification.
            group_name (str): Filters by group name.

        Returns:
            Dict: Response from the API containing the tags.
        """
        params = assign_params(
            type=group_type,
            account_names=",".join(account_names),
            domains=",".join(domains),
            host_ids=",".join(host_ids),
            host_names=",".join(host_names),
            importance=importance,
            ips=",".join(ips),
            description=description,
            name=group_name,
            last_modified_timestamp=last_modified_timestamp,
            last_modified_by=last_modified_by,
        )

        return self.http_request(method="GET", url_suffix=API_ENDPOINT_GROUPS, params=params)

    def close_detection_by_id(self, id: str, reason: str):
        """
        Close a single detection by ID

        - params:
            - id: Vectra Detection ID
            - reason: Close reason (benign or remediated)
        - returns:
            Vectra API call result
        """
        json_payload = {"reason": reason}

        # Execute request
        return self.http_request(method="PATCH", url_suffix=f"{API_ENDPOINT_DETECTIONS}/{id}/close", json_data=json_payload)

    def open_detection_by_id(self, id: str):
        """
        Open a single detection by ID

        - params:
            - id: Vectra Detection ID
        - returns:
            Vectra API call result
        """

        # Execute request
        return self.http_request(method="PATCH", url_suffix=f"{API_ENDPOINT_DETECTIONS}/{id}/open")


# ####                 #### #
# ##  HELPER FUNCTIONS   ## #
#                           #


def str2bool(value: str | None) -> bool | None:
    """
    Converts a string into a boolean

    - params:
        - value: The string to convert
    - returns:
        True if value matches the 'true' list
        False if value matches the 'false' list
        None instead
    """
    if value is None:
        output = None
    elif value.lower() in ("true", "yes"):
        output = True
    elif value.lower() in ("false", "no"):
        output = False
    else:
        output = None

    return output


def sanitize_max_results(max_results=None) -> int:
    """
    Cleans max_results value and ensure it's always lower than the MAX

    - params:
        max_results: The max results number
    - returns:
        The checked/enforced max results value
    """
    if max_results and isinstance(max_results, str):
        max_results = int(max_results)

    if (not max_results) or (max_results > MAX_RESULTS) or (max_results <= 0):
        return MAX_RESULTS
    else:
        return max_results


def scores_to_severity(threat: int | None, certainty: int | None) -> str:
    """
    Converts Vectra scores to a severity String

    - params:
        - threat: The Vectra threat score
        - certainty: The Vectra certainty score
    - returns:
        The severity as text
    """
    severity = "Unknown"
    if isinstance(threat, int) and isinstance(certainty, int):
        if threat < 50 and certainty < 50:
            severity = "Low"
        elif threat < 50:  # and certainty >= 50
            severity = "Medium"
        elif certainty < 50:  # and threat >= 50
            severity = "High"
        else:  # threat >= 50 and certainty >= 50
            severity = "Critical"

    return unify_severity(severity)


def severity_string_to_int(severity: str | None) -> int:
    """
    Converts a severity String to XSOAR severity value

    - params:
        - severity: The severity as text
    - returns:
        The XSOAR severity value
    """
    output = 0
    if severity == "Critical":
        output = 4
    elif severity == "High":
        output = 3
    elif severity == "Medium":
        output = 2
    elif severity == "Low":
        output = 1

    return output


def convert_date(date: str | None) -> str | None:
    """
    Converts a date format to an ISO8601 string

    Converts the Vectra date (YYYY-mm-ddTHH:MM:SSZ) format in a datetime.

    :type date: ``str``
    :param date: a string with the format 'YYYY-mm-DDTHH:MM:SSZ'

    :return: Parsed time in ISO8601 format
    :rtype: ``str``
    """
    if date:
        date_dt = dateparser.parse(str(date))
        if date_dt:
            return date_dt.strftime(DATE_FORMAT)
        else:
            return None
    else:
        return None


def remove_space_from_args(args):
    """Remove space from args."""
    for key in args:
        if isinstance(args[key], str):
            args[key] = args[key].strip()
    return args


def validate_argument(label: str | None, value: Any) -> int:
    """
    Validates a command argument based on its type

    - params:
        - label: The argument label
        - value: The argument value
    - returns:
        The value if OK or raises an Exception if not
    """

    demisto.debug(f"Testing '{label}' argument value")
    if label in ["min_id", "max_id"]:
        try:
            if (value is None) or isinstance(value, float):
                raise ValueError("Cannot be empty or a float")
            if value and isinstance(value, str):
                value = int(value)
            if not isinstance(value, int):
                raise ValueError("Should be an int")
            if int(value) <= 0:
                raise ValueError("Should be > 0")
        except ValueError:
            raise ValueError(f'"{label}" must be an integer greater than 0')
    elif label in ["min_threat", "min_certainty", "max_threat", "max_certainty"]:
        try:
            if (value is None) or isinstance(value, float):
                raise ValueError("Cannot be empty or a float")
            if value and isinstance(value, str):
                value = int(value)
            if not isinstance(value, int):
                raise ValueError("Should be an int")
            if int(value) < 0:
                raise ValueError("Should be >= 0")
            if int(value) > 99:
                raise ValueError("Should be < 100")
        except ValueError:
            raise ValueError(f'"{label}" must be an integer between 0 and 99')
    elif label in ["min_privilege_level"]:
        try:
            if (value is None) or isinstance(value, float):
                raise ValueError("Cannot be empty or a float")
            if value and isinstance(value, str):
                value = int(value)
            if not isinstance(value, int):
                raise ValueError("Should be an int")
            if int(value) < 1:
                raise ValueError("Should be >= 1")
            if int(value) > 10:
                raise ValueError("Should be <= 10")
        except ValueError:
            raise ValueError(f'"{label}" must be an integer between 1 and 10')
    else:
        raise SystemError("Unknown argument type")
    return value


def validate_min_max(min_label: str = None, min_value: str = None, max_label: str = None, max_value: str = None):  # type: ignore
    """
    Validates min/max values for a specific search attribute and ensure max_value >= min_value

    - params:
        - min_label: The attribute label for the min value
        - min_value: The min value
        - max_label: The attribute label for the max value
        - max_value: The max value
    - returns:
        Return True if OK or raises Exception if not
    """
    if min_value:
        validate_argument(min_label, min_value)

    if max_value:
        validate_argument(max_label, max_value)

    if min_value and max_value and int(min_value) > int(max_value):
        raise ValueError(f'"{max_label}" must be greater than or equal to "{min_label}"')

    return True


def validate_fetch_incident_params(integration_params: dict):
    """
    Validates the integration parameters for fetching incidents, including timestamps, entity types, fetch queries,
    and the maximum number of incidents per fetch. Raises ValueErrors for invalid parameters.

    - params:
        - integration_params: A dictionary containing the integration parameters.

    - raises:
        ValueError: If the integration parameters are invalid.
    """
    demisto.debug("Fetching mode is enabled. Testing settings ...")

    demisto.debug("Testing Fetch first timestamp ...")
    fetch_first_time = integration_params.get("first_fetch", DEFAULT_FIRST_FETCH)
    look_back = integration_params.get("look_back", BACK_IN_TIME_SEARCH_IN_MINUTES)
    demisto.debug(f"Fetch first timestamp : {fetch_first_time}")
    try:
        iso_date_to_vectra_start_time(fetch_first_time, look_back)
    except SystemError as exc:
        raise ValueError("Fetch first timestamp is invalid.") from exc
    demisto.debug("Testing Fetch first timestamp [done]")

    demisto.debug("Testing Fetch entity types ...")
    fetch_entity_types = integration_params.get("fetch_entity_types", DEFAULT_FETCH_ENTITY_TYPES)
    demisto.debug(f"Fetch entity types : {fetch_entity_types}")
    if len(fetch_entity_types) == 0:
        raise ValueError("You must select at least one entity type to fetch.")
    for entity_itt in fetch_entity_types:
        if entity_itt not in ENTITY_TYPES:
            raise ValueError(f'This entity type "{entity_itt}" is invalid.')
    demisto.debug("Testing Fetch entity types [done]")

    accounts_fetch_query = integration_params.get("accounts_fetch_query")
    demisto.debug(f"'Accounts' fetch query : {accounts_fetch_query}")

    hosts_fetch_query = integration_params.get("hosts_fetch_query")
    demisto.debug(f"'Hosts' fetch query : {hosts_fetch_query}")

    detections_fetch_query = integration_params.get("detections_fetch_query")
    demisto.debug(f"'Detections' fetch query : {detections_fetch_query}")

    demisto.debug("Testing Max incidents per fetch ...")
    max_incidents_per_fetch = integration_params.get("max_fetch", DEFAULT_MAX_FETCH)
    demisto.debug(f"Max incidents per fetch (initial value): {max_incidents_per_fetch}")
    if isinstance(max_incidents_per_fetch, str):
        try:
            max_incidents_per_fetch = int(max_incidents_per_fetch)
        except ValueError as exc:
            raise ValueError(ERRORS["INVALID_MAX_FETCH"].format(max_incidents_per_fetch)) from exc
    if max_incidents_per_fetch <= 0:
        raise ValueError(ERRORS["INVALID_MAX_FETCH"].format(max_incidents_per_fetch))

    if (max_incidents_per_fetch // len(fetch_entity_types)) == 0:
        raise ValueError(
            f"Max incidents per fetch ({max_incidents_per_fetch}) must be >= "
            f"to the number of entity types you're fetching ({len(fetch_entity_types)})"
        )

    demisto.debug(f"Max incidents per fetch (final value): {max_incidents_per_fetch}")
    demisto.debug("Testing Max incidents per fetch [done]")


def build_search_query_for_detections(
    entity_id: str, fetch_type: str, detection_category: str = "", detection_type: str = ""
) -> str:
    """
    Builds a search query for tags.

    - params:
        - entity_id: The ID of the entity.
        - fetch_type: The type of entity.
        - detection_category: The category of the detection.
        - detection_type: The type of the detection.
    - returns:
        Returns the search query.
    """
    search_query = f"detection.src_linked_{fetch_type}.id:{entity_id}"
    if detection_category:
        search_query += f' AND detection.detection_category:"{detection_category}"'
    if detection_type:
        search_query += f' AND detection.detection_type:"{detection_type}"'
    return search_query


def build_search_query_for_tags(fetch_query: str, fetch_type: str, tags: list[str]) -> str:
    """
    Builds a search query for tags.

    - params:
        - fetch_query: The search query.
        - fetch_type: The type of entity.
        - tags: The list of tags.
    - returns:
        Returns the search query.
    """
    if not tags:
        return fetch_query
    first_tag = tags[0]
    if "*" not in first_tag:
        first_tag = f'"{first_tag}"'
    tag_query = f"({fetch_type}.tags:{first_tag}"
    if len(tags) > 1:
        for tag in tags[1:]:
            if "*" in tag:
                tag_query += f" OR {fetch_type}.tags:{tag}"
            else:
                tag_query += f' OR {fetch_type}.tags:"{tag}"'
    tag_query += ")"
    if fetch_query:
        return f"{fetch_query} AND {tag_query}"
    return tag_query


def sanitize_str_ids_list_to_set(list: str | None) -> set[int] | None:
    """
    Sanitize the given list to ensure all IDs are valid

    - params:
        - list: The list to sanitize
    - returns:
        Returns the sanitized list (only valid IDs)
    """
    output: set[int] = set()
    if list is not None and isinstance(list, str):
        ids_list = [id.strip() for id in list.split(",")]
        for id in ids_list:
            if id != "":
                try:
                    validate_argument("min_id", id)
                except ValueError:
                    raise ValueError(f'ID "{id}" is invalid')
                output.add(int(id))

    if len(output) > 0:
        return output
    else:
        return None


def build_search_query(object_type, params: dict) -> str:
    """
    Builds a Lucene syntax search query depending on the object type to search on (Account, Detection, Host)

    - params:
        - object_type: The object type we're searching (Account, Detection, Host)
        - params: The search params
    - returns:
        The Lucene search query
    """
    query = ""
    attribute = ""
    operator = ""

    for key, value in params.items():
        if key.startswith("min_"):
            operator = ":>="
        elif key.startswith("max_"):
            operator = ":<="

        if key.endswith("_id"):
            attribute = "id"
        elif key.endswith("_threat"):
            attribute = "threat"
        elif key.endswith("_certainty"):
            attribute = "certainty"

        if key in ["state"]:
            attribute = key
            operator = ":"
            value = f'"{value}"'

        if key == "last_timestamp":
            operator = ":>="
            if object_type == "detection":
                attribute = "last_timestamp"
            else:
                attribute = "last_detection_timestamp"

        if key == "doc_modified_time":
            operator = ":>="
            attribute = "_doc_modified_ts"

        # Append query
        # No need to add "AND" as implied
        query += f" {object_type}.{attribute}{operator}{value}"

    return query.strip()


def forge_entity_url(type: str, id: str | None) -> str:
    """
    Generate the UI pivot URL

    - params:
        - type: The object type ("account", "detection" or "host")
        - id: The object ID
    - returns:
        The pivot URL using server FQDN
    """
    if type == "account":
        url_suffix = f"{UI_ACCOUNTS}/"
    elif type == "detection":
        url_suffix = f"{UI_DETECTIONS}/"
    elif type == "host":
        url_suffix = f"{UI_HOSTS}/"
    else:
        raise Exception(f"Unknown type : {type}")

    if not id:
        raise Exception("Missing ID")

    return urljoin(urljoin(global_UI_URL, url_suffix), str(id)) + UTM_PIVOT


def common_extract_data(entity: dict[str, Any]) -> dict[str, Any]:
    """
    Extracts common information from Vectra object renaming attributes on the fly.

    - params:
        - host: The Vectra object
    - returns:
        The extracted data
    """
    return {
        "Assignee": entity.get("assigned_to"),
        "AssignedDate": convert_date(entity.get("assigned_date")),
        "CertaintyScore": entity.get("certainty"),
        "ID": entity.get("id"),
        "State": entity.get("state"),
        "Tags": entity.get("tags"),
        "ThreatScore": entity.get("threat"),
    }


def extract_account_data(account: dict[str, Any]) -> dict[str, Any]:
    """
    Extracts useful information from Vectra Account object renaming attributes on the fly.

    - params:
        - host: The Vectra Account object
    - returns:
        The Account extracted data
    """
    return common_extract_data(account) | {  # type: ignore
        "LastDetectionTimestamp": convert_date(account.get("last_detection_timestamp")),
        "PrivilegeLevel": account.get("privilege_level"),
        "PrivilegeCategory": account.get("privilege_category"),
        "Severity": unify_severity(account.get("severity")),
        "Type": account.get("account_type"),
        "URL": forge_entity_url("account", account.get("id")),
        "Username": account.get("name"),
        "DocModifiedTimestamp": convert_date(account.get("_doc_modified_ts")),
    }


def extract_detection_data(detection: dict[str, Any]) -> dict[str, Any]:
    """
    Extracts useful information from Vectra Detection object renaming attributes on the fly.

    - params:
        - host: The Vectra Detection object
    - returns:
        The Detection extracted data
    """
    # Complex values
    detection_name = detection.get("custom_detection") if detection.get("custom_detection") else detection.get("detection")

    source_account = detection.get("src_account")
    source_account_id = source_account.get("id") if source_account else None

    source_host = detection.get("src_host")
    source_host_id = source_host.get("id") if source_host else None

    summary = detection.get("summary")
    if summary:
        description = summary.get("description")
        dst_ips = summary.get("dst_ips")
        dst_ports = summary.get("dst_ports")
    else:
        description = dst_ips = dst_ports = None

    return common_extract_data(detection) | remove_empty_elements(
        {
            "Category": detection.get("category"),
            "Description": description,
            "DestinationIPs": dst_ips,
            "DestinationPorts": dst_ports,
            "FirstTimestamp": convert_date(detection.get("first_timestamp")),
            "IsTargetingKeyAsset": detection.get("is_targeting_key_asset"),
            "LastTimestamp": convert_date(detection.get("last_timestamp")),
            "Name": detection_name,
            "Severity": scores_to_severity(detection.get("threat"), detection.get("certainty")),
            "SensorLUID": detection.get("sensor"),
            "SensorName": detection.get("sensor_name"),
            "SourceAccountID": source_account_id,
            "SourceHostID": source_host_id,
            "SourceIP": detection.get("src_ip"),
            "TriageRuleID": detection.get("triage_rule_id"),
            "Type": detection.get("detection"),
            "URL": forge_entity_url("detection", detection.get("id")),
        }
    )


def extract_host_data(host: dict[str, Any]) -> dict[str, Any]:
    """
    Extracts useful information from Vectra Host object renaming attributes on the fly.

    - params:
        - host: The Vectra Hosts object
    - returns:
        The Host extracted data
    """
    return common_extract_data(host) | {  # type: ignore
        "HasActiveTraffic": host.get("has_active_traffic"),
        "Hostname": host.get("name"),
        "IPAddress": host.get("ip"),
        "IsKeyAsset": host.get("is_key_asset"),
        "IsTargetingKeyAsset": host.get("is_targeting_key_asset"),
        "LastDetectionTimestamp": convert_date(host.get("last_detection_timestamp")),
        "PrivilegeLevel": host.get("privilege_level"),
        "PrivilegeCategory": host.get("privilege_category"),
        "ProbableOwner": host.get("probable_owner"),
        "SensorLUID": host.get("sensor"),
        "SensorName": host.get("sensor_name"),
        "Severity": unify_severity(host.get("severity")),
        "URL": forge_entity_url("host", host.get("id")),
        "DocModifiedTimestamp": convert_date(host.get("_doc_modified_ts")),
    }


def extract_assignment_data(assignment: dict[str, Any]) -> dict[str, Any]:
    """
    Extracts useful information from Vectra Assignment object renaming attributes on the fly.

    - params:
        - assignment: The Vectra Assignment object
    - returns:
        The Assignment extracted data
    """
    assigned_by = assignment.get("assigned_by")
    assigned_by_user = assigned_by.get("username") if assigned_by else None
    assigned_to = assignment.get("assigned_to")
    assigned_to_user = assigned_to.get("username") if assigned_to else None

    outcome = assignment.get("outcome")
    outcome_title = outcome.get("title") if outcome else None
    outcome_category = outcome.get("category") if outcome else None

    resolved_by = assignment.get("resolved_by")
    resolved_by_user = resolved_by.get("username") if resolved_by else None

    # assignment['events'][0]['context'] is always present
    triaged_as = assignment["events"][0]["context"].get("triage_as")

    return remove_empty_elements(
        {
            "AccountID": assignment.get("account_id"),
            "AssignedBy": assigned_by_user,
            "AssignedDate": convert_date(assignment.get("date_assigned")),
            "AssignedTo": assigned_to_user,
            "HostID": assignment.get("host_id"),
            "ID": assignment.get("id"),
            "IsResolved": assignment.get("resolved_by") is not None,
            "OutcomeCategory": convert_outcome_category_raw2text(outcome_category),
            "OutcomeTitle": outcome_title,
            "TriagedDetections": assignment.get("triaged_detections"),
            "TriagedAs": triaged_as,
            "ResolvedBy": resolved_by_user,
            "ResolvedDate": convert_date(assignment.get("date_resolved")),
        }
    )


def extract_outcome_data(outcome: dict[str, Any]) -> dict[str, Any]:
    """
    Extracts useful information from Vectra Outcome object renaming attributes on the fly.

    - params:
        - outcome: The Vectra Outcome object
    - returns:
        The Outcome extracted data
    """
    return {
        "Category": convert_outcome_category_raw2text(outcome.get("category")),
        "ID": outcome.get("id"),
        "IsBuiltIn": outcome.get("builtin"),
        "Title": outcome.get("title"),
    }


def extract_user_data(user: dict[str, Any]) -> dict[str, Any]:
    """
    Extracts useful information from Vectra User object renaming attributes on the fly.

    - params:
        - user: The Vectra User object
    - returns:
        The User extracted data
    """
    return {
        "Email": user.get("email"),
        "ID": user.get("id"),
        "Role": user.get("role"),
        "Type": user.get("account_type"),
        "Username": user.get("username"),
        "LastLoginDate": convert_date(user.get("last_login")),
    }


def detection_to_incident(detection: dict):
    """
    Creates an incident of a Detection.

    :type detection: ``dict``
    :param detection: Single detection object

    :return: Incident representation of a Detection
    :rtype ``dict``
    """

    extracted_data = extract_detection_data(detection)

    incident_name = f"Vectra Detection ID: {extracted_data.get('ID')} - {extracted_data.get('Name')}"

    vectra_specific = {
        "entity_type": extracted_data.get("Category"),
        "UI_URL": extracted_data.get("URL"),
    }
    detection.update({"_vectra_specific": vectra_specific})

    incident = {
        "name": incident_name,  # name is required field, must be set
        "occurred": extracted_data.get("LastTimestamp"),  # must be string of a format ISO8601
        "rawJSON": json.dumps(detection),  # the original event,
        #   this will allow mapping of the event in the mapping stage.
        #   Don't forget to `json.dumps`
        "severity": severity_string_to_int(extracted_data.get("Severity")),
        # 'dbotMirrorId': extracted_data.get('ID')
    }

    incident_last_run = {
        "last_timestamp": dateparser.parse(
            extracted_data.get("LastTimestamp"),  # type: ignore
            settings={"TO_TIMEZONE": "UTC"},
        ).isoformat(),  # type: ignore
        "id": extracted_data.get("ID"),
    }

    return incident, incident_last_run


def host_to_incident(
    client: Client,
    host: dict,
    detection_category: str = "",
    detection_type: str = "",
    fetch_escalated_accounts_and_hosts: bool = False,
):
    """
    Creates an incident of a Host.

    :type client: ``Client``
    :param client: The client object used to communicate with the Vectra API.

    :type host: ``dict``
    :param host: Single Host object.

    :type detection_category: ``str``
    :param detection_category: The category of the detection.

    :type detection_type: ``str``
    :param detection_type: The type of the detection.

    :type fetch_escalated_accounts_and_hosts: ``bool``
    :param fetch_escalated_accounts_and_hosts: Whether to fetch escalated accounts and hosts.

    :return: Incident representation of a Host.
    :rtype ``dict``
    """

    extracted_data = extract_host_data(host)

    incident_name = f"Vectra Host ID: {extracted_data.get('ID')} - {extracted_data.get('Hostname')}"

    detections = []
    detections_data = client.list_detections_by_host_id(str(host.get("id")), "active", detection_category, detection_type)
    detections = detections_data.get("results", [])
    for detection in detections:
        detection["url"] = forge_entity_url("detection", detection.get("id"))
    demisto.debug(f"Found {len(detections)} detection(s) for the host with the ID: {host.get('id')}.")
    host.update({"detection_details": detections})

    response = client.list_assignments_request(hosts=host.get("id"), page_size=1)
    assignment_details = response.get("results", [])
    assignment_details = assignment_details[0] if len(assignment_details) > 0 else {}
    if assignment_details:
        if not assignment_details.get("resolved_by"):
            assignment_details["resolved_by"] = {"username": ""}
            assignment_details["outcome"] = {"title": ""}
            assignment_details["date_resolved"] = ""
    else:
        assignment_details = EMPTY_ASSIGNMENT
    host.update({"assignment_details": assignment_details})

    vectra_specific = {
        "entity_type": "host",
        "UI_URL": extracted_data.get("URL"),
    }
    host.update({"_vectra_specific": vectra_specific})

    mirroring_fields = get_mirroring()
    mirroring_fields.update({"mirror_id": str(host.get("id")) + "-" + "host"})
    host.update(mirroring_fields)
    calculated_severity = severity_string_to_int(scores_to_severity(host.get("threat"), host.get("certainty")))
    host.update({"calculated_severity": calculated_severity})

    incident = {
        "name": incident_name,  # name is required field, must be set
        "occurred": extracted_data.get("LastDetectionTimestamp"),  # must be string of a format ISO8601
        "rawJSON": json.dumps(host),  # the original event,
        #   this will allow mapping of the event in the mapping stage.
        #   Don't forget to `json.dumps`
        "severity": host.get("calculated_severity"),
        # 'dbotMirrorId': extracted_data.get('ID')
    }

    incident_last_run = {
        "last_timestamp": dateparser.parse(
            (
                extracted_data.get("DocModifiedTimestamp")  # type: ignore
                if fetch_escalated_accounts_and_hosts
                else extracted_data.get("LastDetectionTimestamp")
            ),
            settings={"TO_TIMEZONE": "UTC"},
        ).isoformat(),
        "id": extracted_data.get("ID"),
    }

    return incident, incident_last_run


def account_to_incident(
    client: Client,
    account: dict,
    detection_category: str = "",
    detection_type: str = "",
    fetch_escalated_accounts_and_hosts: bool = False,
):
    """
    Creates an incident of an Account.

    :type client: ``Client``
    :param client: The client object used to communicate with the Vectra API.

    :type host: ``dict``
    :param host: Single Account object

    :type detection_category: ``str``
    :param detection_category: The category of the detection.

    :type detection_type: ``str``
    :param detection_type: The type of the detection.

    :type fetch_escalated_accounts_and_hosts: ``bool``
    :param fetch_escalated_accounts_and_hosts: Whether to fetch escalated accounts and hosts.

    :return: Incident representation of a Account
    :rtype ``dict``
    """

    extracted_data = extract_account_data(account)
    account_id: str = extracted_data.get("ID", "")

    incident_name = f"Vectra Account ID: {account_id} - {account.get('display_name')}"

    search_query = build_search_query_for_detections(account_id, "account", detection_category, detection_type)
    api_response = client.search_detections(state="active", search_query=search_query)
    if (api_response is None) or (api_response.get("count") is None):
        raise VectraException("API issue - Response is empty or invalid")

    detections = []
    if api_response.get("count") == 0:
        demisto.info(f"Found 0 detection(s) for the account with the ID: {account_id}.")
    elif api_response.get("count", 0) > 0:
        demisto.info(f"Found {api_response.get('count')} detection(s) for the account with the ID: {account_id}.")

        if api_response.get("results") is None:
            raise VectraException("API issue - Response is empty or invalid")

        # Due to backward search we need to avoid creating incidents of already ingested events
        detections = api_response.get("results", [])
        for detection in detections:
            detection["url"] = forge_entity_url("detection", detection.get("id"))

    groups_response = client.list_group_request(group_type="account", account_names=[account.get("display_name")])  # type: ignore

    account.update({"groups": groups_response.get("results", [])})

    # Add detection details to the entity
    account.update({"detection_details": detections})

    response = client.list_assignments_request(accounts=account.get("id"), page_size=1)
    assignment_details = response.get("results", [])
    assignment_details = assignment_details[0] if len(assignment_details) > 0 else {}
    if assignment_details:
        if not assignment_details.get("resolved_by"):
            assignment_details["resolved_by"] = {"username": ""}
            assignment_details["outcome"] = {"title": ""}
            assignment_details["date_resolved"] = ""
    else:
        assignment_details = EMPTY_ASSIGNMENT
    account.update({"assignment_details": assignment_details})

    vectra_specific = {
        "entity_type": "account",
        "UI_URL": extracted_data.get("URL"),
    }
    account.update({"_vectra_specific": vectra_specific})

    mirroring_fields = get_mirroring()
    mirroring_fields.update({"mirror_id": str(account.get("id")) + "-" + "account"})
    account.update(mirroring_fields)
    calculated_severity = severity_string_to_int(scores_to_severity(account.get("threat"), account.get("certainty")))
    account.update({"calculated_severity": calculated_severity})
    incident = {
        "name": incident_name,  # name is required field, must be set
        "occurred": extracted_data.get("LastDetectionTimestamp"),  # must be string of a format ISO8601
        "rawJSON": json.dumps(account),  # the original event,
        #   this will allow mapping of the event in the mapping stage.
        #   Don't forget to `json.dumps`
        "severity": account.get("calculated_severity"),
    }

    incident_last_run = {
        "last_timestamp": dateparser.parse(
            (
                extracted_data.get("DocModifiedTimestamp")  # type: ignore
                if fetch_escalated_accounts_and_hosts
                else extracted_data.get("LastDetectionTimestamp")
            ),
            settings={"TO_TIMEZONE": "UTC"},
        ).isoformat(),
        "id": account_id,
    }

    return incident, incident_last_run


def get_last_run_details(integration_params: dict, is_test: bool = False) -> dict:
    """
    Extracts detail from the stored last_run variable or create them if needed

    :type integration_params: ``dict``
    :param integration_params: The integration configuration parameters.

    :type is_test: ``bool``
    :param is_test: A boolean indicating whether the command is being run in test mode.

    :return: Last run content.
    :rtype ``dict``
    """
    # Get the config settings
    fetch_first_time = integration_params.get("first_fetch", DEFAULT_FIRST_FETCH)
    fetch_entity_types = integration_params.get("fetch_entity_types", DEFAULT_FETCH_ENTITY_TYPES)

    # Get the last run value
    last_run = {} if is_test else demisto.getLastRun()
    demisto.debug(f"last run : {last_run}")

    output_last_run: dict = {}
    for entity_type in ENTITY_TYPES:
        if entity_type in fetch_entity_types:
            # This will return a relative TZaware datetime (in UTC)
            last_timestamp = dateparser.parse(
                fetch_first_time,  # type: ignore
                settings={"TO_TIMEZONE": "UTC"},
            ).isoformat()  # type: ignore
            last_id = 0
            empty_last_run: dict = {"last_timestamp": last_timestamp, "id": last_id, "last_created_events": []}
            output_last_run[entity_type] = empty_last_run
            if not last_run.get(entity_type):
                demisto.debug(f"Last run is not set for '{entity_type}'. Using value from config : {fetch_first_time}")
                # This will return a relative TZaware datetime (in UTC)
                demisto.debug(f"New last run for {entity_type}, {output_last_run[entity_type]}")
            else:
                output_last_run[entity_type].update(last_run.get(entity_type, {}))

        elif last_run.get(entity_type):
            demisto.debug(f"'{entity_type} present in last run but no more used, discarding.")

    return output_last_run


def iso_date_to_vectra_start_time(iso_date: str, look_back: str = BACK_IN_TIME_SEARCH_IN_MINUTES):
    """
    Converts an iso date into a Vectra timestamp used in search query.

    - params:
        - iso_date: The ISO date to convert
        - look_back: The look back time in minutes.
    - returns:
        A Vectra date timestamp.
    """
    # This will return a relative TZaware datetime (in UTC)
    date = dateparser.parse(iso_date, settings={"TO_TIMEZONE": "UTC"})  # type: ignore

    if date:
        # We should return time in YYYY-MM-DDTHHMM format for Vectra Lucene query search ...
        start_datetime = date.strftime(r"%Y-%m-%dT%H%M")
        demisto.debug(f"Start datetime is : {start_datetime}")

        if look_back:
            look_back_int = arg_to_number(look_back, "look_back", True)
            if look_back_int < 0:  # type: ignore
                raise ValueError(ERRORS["POSITIVE_VALUE"].format("look back"))
            demisto.debug(f"The look back is : {look_back_int} minutes. ")
            if look_back_int != 0:
                # Manipulate the date if we need to search backward
                # Timedelta is imported from CommonServerPython
                date = date - timedelta(minutes=look_back_int)  # type: ignore
                backward_start_datetime = date.strftime("%Y-%m-%dT%H%M")  # type: ignore
                demisto.debug(
                    "Manipulated time as backward search. "
                    f"Changed the Start time from : {start_datetime} to : {backward_start_datetime}"
                )
                start_datetime = backward_start_datetime

    else:
        raise SystemError("Invalid ISO date")

    return start_datetime


def unify_severity(severity: str | None) -> str:
    """
    Force severity string to be consistent across endpoints

    - params:
        - severity: The severity string
    - returns:
        The unified severity string (First capitalized letter)
    """
    if severity:
        output = severity.capitalize()
    else:
        output = "Unknown"

    return output


def convert_outcome_category_raw2text(category: str | None) -> str | None:
    """
    Convert outcome category from raw to human readable text

    - params:
        - category: The raw outcome category string
    - returns:
        The human readable outcome category string
    """
    return OUTCOME_CATEGORIES.get(category) if category else None


def convert_outcome_category_text2raw(category: str) -> str | None:
    """
    Convert outcome category from human readable text to raw

    - params:
        - category: The human readable outcome category string
    - returns:
        The raw outcome category string
    """
    # Inverting Key/Value
    category_text = {v: k for k, v in OUTCOME_CATEGORIES.items()}
    return category_text.get(category) if category else None


def validate_positive_integer_arg(value: Any | None, arg_name: str, required: bool = False) -> bool:
    """
    Validates whether the provided argument value is a valid positive integer.

    Args:
        value (int): The value to validate.
        arg_name (str): The name of the argument.
        required (bool): Flag indicating if the argument is required (default: False).

    Returns:
        bool: True if the value is a valid positive integer.

    Raises:
        ValueError: If the value is not a valid positive integer.
    """
    if required and not value:
        raise ValueError(ERRORS["REQUIRED_ARGUMENT"].format(arg_name))
    if value is not None and (not str(value).isdigit() or int(value) <= 0):
        raise ValueError(ERRORS["INVALID_INTEGER_VALUE"].format(arg_name))

    return True


def validate_group_list_command_args(args: dict[Any, Any]):
    """
    Validates the arguments provided for the group list command.

    Args:
        args (dict[Any, Any]): The arguments dictionary.

    Raises:
        ValueError: If any of the arguments are invalid.
    """
    group_type = args.get("group_type", "")
    if group_type and isinstance(group_type, str):
        group_type = group_type.lower()
        # Validate group_type value
        if group_type not in VALID_GROUP_TYPE:
            raise ValueError(ERRORS["INVALID_COMMAND_ARG_VALUE"].format("group_type", ", ".join(VALID_GROUP_TYPE)))

    importance = args.get("importance", "")
    # Validate importance value
    if importance and isinstance(importance, str) and importance.lower() not in VALID_IMPORTANCE_VALUE:
        raise ValueError(ERRORS["INVALID_COMMAND_ARG_VALUE"].format("importance", ", ".join(VALID_IMPORTANCE_VALUE)))

    # Validate account_names value
    account_names = argToList(args.get("account_names", ""))
    if account_names and group_type != "account":
        raise ValueError(ERRORS["INVALID_SUPPORT_FOR_ARG"].format("group_type", "account", "account_names"))

    # Validate domains value
    domains = argToList(args.get("domains", ""))
    if domains and group_type != "domain":
        raise ValueError(ERRORS["INVALID_SUPPORT_FOR_ARG"].format("group_type", "domain", "domains"))

    # Validate host_ids value
    host_ids = argToList(args.get("host_ids", ""))
    if host_ids and group_type != "host":
        raise ValueError(ERRORS["INVALID_SUPPORT_FOR_ARG"].format("group_type", "host", "host_ids"))
    for host_id in host_ids:
        host_id = arg_to_number(host_id, "host_ids")
        validate_positive_integer_arg(host_id, arg_name="host_ids")

    # Validate host_names value
    host_names = argToList(args.get("host_names", ""))
    if host_names and group_type != "host":
        raise ValueError(ERRORS["INVALID_SUPPORT_FOR_ARG"].format("group_type", "host", "host_names"))

    # Validate ips value
    ips = argToList(args.get("ips", ""))
    if ips and group_type != "ip":
        raise ValueError(ERRORS["INVALID_SUPPORT_FOR_ARG"].format("group_type", "ip", "ips"))


def get_group_list_command_hr(groups: list):
    """
    Converts a list of groups into a human-readable table format.

    Args:
        groups (Dict): The list of groups to convert.

    Returns:
        str: The human-readable table in Markdown format.
    """
    hr_dict = []
    # Process members data from group and make HR for groups
    for group in groups:  # type: ignore
        group["group_id"] = group["id"]
        members: list = group.get("members")
        members_hr = None
        if members and isinstance(members, list):
            # If the members are simple list of strings, then join them with comma.
            if isinstance(members[0], str):
                members_hr = ", ".join([(str(member)) for member in members])
            # If the members are list of dictionaries, then extract important field from that and join it with comma.
            elif isinstance(members[0], dict):
                members_list = []
                for member in members:
                    if member.get("uid"):
                        members_list.append(str(member.get("uid")))  # type: ignore
                    elif member.get("id"):
                        members_list.append(  # type: ignore
                            f"[{member.get('id')}]({forge_entity_url(group.get('type'), member.get('id'))})"
                        )
                members_hr = ", ".join(members_list)

        hr_dict.append(
            {
                "Group ID": group.get("group_id"),
                "Name": group.get("name"),
                "Group Type": group.get("type"),
                "Description": group.get("description"),
                "Importance": group.get("importance"),
                "Members": members_hr,
                "Last Modified Timestamp": group.get("last_modified"),
            }
        )
    # Prepare human-readable output table
    human_readable = tableToMarkdown(
        "Groups Table",
        hr_dict,
        ["Group ID", "Name", "Group Type", "Description", "Importance", "Members", "Last Modified Timestamp"],
        removeNull=True,
    )

    return human_readable


def validate_group_assign_and_unassign_command_args(args):
    """
    Validate the arguments provided for assigning or unassigning members to/from a group.

    Args:
        args (Dict): A dictionary containing the arguments for the group assign and unassign command.

    Raises:
        ValueError: If the provided group_id is not a positive integer.
        ValueError: If members argument is missing.
    """
    group_id = args.get("group_id")
    members = args.get("members")
    validate_positive_integer_arg(group_id, arg_name="group_id", required=True)

    if not members:
        raise ValueError(ERRORS["REQUIRED_ARGUMENT"].format("members"))


def get_group_unassign_and_assign_command_hr(group: dict, changed_members: list, assign_flag: bool = False):
    """
    Converts group into a human-readable table format.

    Args:
        group (Dict): The group to convert.
        changed_members (List): Removed/Added members from the group.
        assign_flag (bool): True for unassigning members, False for assigning members.

    Returns:
        str: The human-readable table in Markdown format.
    """
    hr_dict = []
    group["group_id"] = group["id"]
    members = group.get("members")
    members_hr = None
    if members and isinstance(members, list):
        # If the members are simple list of strings, then join them with comma.
        if isinstance(members[0], str):
            members_hr = ", ".join([(str(member)) for member in members])
        # If the members are list of dictionaries, then extract important field from that and join it with comma.
        elif isinstance(members[0], dict):
            members_list = []
            for member in members:
                if member.get("uid"):
                    members_list.append(str(member.get("uid")))  # type: ignore
                elif member.get("id"):
                    members_list.append(  # type: ignore
                        f"[{member.get('id')}]({forge_entity_url(str(group.get('type')), member.get('id'))})"
                    )
            members_hr = ", ".join(members_list)

    hr_dict.append(
        {
            "Group ID": group.get("group_id"),
            "Name": group.get("name"),
            "Group Type": group.get("type"),
            "Description": group.get("description"),
            "Members": members_hr,
            "Last Modified Timestamp": group.get("last_modified"),
        }
    )

    if group.get("type").lower() == "account" and assign_flag is True:  # type: ignore
        new_changed_members = []
        ignored_members = []
        accounts = [account.get("uid") for account in group.get("members", [])]
        for member in changed_members:
            if member in accounts:
                new_changed_members.append(member)
            else:
                ignored_members.append(member)

        if ignored_members:
            return_warning(
                f"The following account names were invalid: {', '.join(ignored_members)}",
                exit=len(ignored_members) == len(changed_members),
            )

        changed_members = new_changed_members

    # Prepare human-readable output table
    change_action = "assigned to" if assign_flag else "unassigned from"
    human_readable = tableToMarkdown(
        f"Member(s) {', '.join(changed_members)} have been {change_action} the group.\n### Updated group details:",
        hr_dict,
        ["Group ID", "Name", "Group Type", "Description", "Members", "Last Modified Timestamp"],
        removeNull=True,
    )

    return human_readable


def validate_note_add_command_args(entity_id: int | None, note: str, entity_type: str):
    """
    Validates the arguments provided for the note add command.

    Args:
        entity_id (int): The ID of the object.
        note (str): The note to add.

    Raises:
        ValueError: If any of the arguments are invalid.
    """

    # Validate id value
    validate_positive_integer_arg(entity_id, arg_name=f"{entity_type}_id", required=True)

    # Validate note value
    if not note:
        raise ValueError(ERRORS["REQUIRED_ARGUMENT"].format("note"))


def validate_note_update_command_args(entity_id: int | None, note_id: int | None, note: str, entity_type: str):
    """
    Validates the arguments provided for the note update command.

    Args:
        entity_id (int): The ID of the object.
        note_id (int): The ID of the note to be updated.
        note (str): The note to update.

    Raises:
        ValueError: If any of the arguments are invalid.
    """

    # Validate entity_id and note_id value
    validate_positive_integer_arg(entity_id, arg_name=f"{entity_type}_id", required=True)
    validate_positive_integer_arg(note_id, arg_name="note_id", required=True)

    # Validate note value
    if not note:
        raise ValueError(ERRORS["REQUIRED_ARGUMENT"].format("note"))


def validate_note_remove_command_args(entity_id: int | None, note_id: int | None, entity_type: str):
    """
    Validates the arguments provided for the note remove command.

    Args:
        entity_id (int): The ID of the object.
        note_id (int): The ID of the note to be removed.
        entity_type (str): The type from Account, Host or Detection.

    Raises:
        ValueError: If any of the arguments are invalid.
    """

    # Validate entity_id value
    validate_positive_integer_arg(entity_id, arg_name=f"{entity_type}_id", required=True)

    # Validate note_id value
    validate_positive_integer_arg(note_id, arg_name="note_id", required=True)

    return True


def get_list_notes_command_hr(notes: dict, entity_id: int | None, entity_type: str) -> str:  # type: ignore
    """
    Returns the human-readable output for the Account, Host or Detection notes.

    Args:
        notes (Dict): The assignment details dictionary.
        entity_id (Optional[int]): Account/Host/Detection ID.
        entity_type (str): Account, Host or Detection.

    Returns:
        str: The human-readable output.
    """
    hr_dict = []
    for note in notes:
        note.update({"note_id": note["id"]})
        del note["id"]
        note.update({f"{entity_type}_id": entity_id})

        hr_dict.append(
            {
                "Note ID": note.get("note_id"),
                "Note": note.get("note"),
                "Created By": note.get("created_by"),
                "Created Date": note.get("date_created"),
                "Modified By": note.get("modified_by"),
                "Modified Date": note.get("date_modified"),
            }
        )

    # Prepare human-readable output table
    human_readable = tableToMarkdown(
        "Notes Table", hr_dict, ["Note ID", "Note", "Created By", "Created Date", "Modified By", "Modified Date"], removeNull=True
    )
    return human_readable


def get_mirroring():
    """
    Get the mirroring configuration parameters from the Demisto integration parameters.

    Returns:
        dict: A dictionary containing the mirroring configuration parameters.
    """
    params = demisto.params()
    mirror_direction = params.get("mirror_direction", "None").strip()
    mirror_tags = params.get("note_tag", "").strip()
    return {
        "mirror_direction": MIRROR_DIRECTION.get(mirror_direction),
        "mirror_tags": mirror_tags,
        "mirror_instance": demisto.integrationInstance(),
    }


def reopen_in_xsoar(entries: list, entity_id_type: list):
    """Reopen the XSOAR incident for the given entity.

    Args:
        entries (list): List of entries where the reopening entry will be appended.
        entity_id_type (list): Indicates the entity ID and type.
    """
    demisto.debug(f"Reopening the incident with remote entity ID: {entity_id_type}.")
    entries.append({"Type": EntryType.NOTE, "Contents": {"dbotIncidentReopen": True}, "ContentsFormat": EntryFormat.JSON})


def add_notes_to_new_entries(notes: list, command_last_run_dt: datetime | None) -> list:
    """
    Lists notes from vectra that to be added when mirrored on xsoar

    Args:
        notes (list): Notes from vectra platform
        command_last_run_dt (str): last time when command was ran on XSOAR

    Returns:
        list: list of notes to be added in XSOAR
    """
    if not notes:
        return []

    new_entry_notes: list[dict] = []
    for note in notes:
        if "[Mirrored From XSOAR]" in note.get("note"):
            demisto.debug(f"Skipping the note {note.get('id')} as it is mirrored from XSOAR.")
            continue
        note_date_modified = arg_to_datetime(note.get("date_modified"))
        if note_date_modified and note_date_modified <= command_last_run_dt:  # type: ignore
            demisto.debug(f"Skipping the note {note.get('id')} as it was modified earlier than the command last run timestamp.")
            continue
        else:
            note_date_created = arg_to_datetime(note.get("date_created"), arg_name="date_created", required=True)
            if note_date_created <= command_last_run_dt:  # type: ignore
                demisto.debug(f"Skipping the note {note.get('id')} as it is older than the command last run timestamp.")
                continue

        if "\n" in note.get("note"):
            note_info = f"\n{note.get('note')}"
        else:
            note_info = note.get("note")

        new_entry_notes.append(
            {
                "Type": EntryType.NOTE,
                "Contents": f"[Mirrored From Vectra]\n"
                f"Added By: {note.get('created_by')}\n"
                f"Added At: {note.get('date_created')} UTC\n"
                f"Note: {note_info}",
                "ContentsFormat": EntryFormat.MARKDOWN,
                "Note": True,
            }
        )
    return new_entry_notes


def add_refetch_id_to_integration_context(entity_id: str, entity_type: str):
    """
    Adds the entity ID and type to the integration context.
    Args:
        entity_id (str): The ID of the entity.
        entity_type (str): The type of the entity.
    """
    if not entity_id or not entity_type:
        raise ValueError("Both 'entity_id' and 'entity_type' arguments are required.")

    if entity_type not in ENTITY_TYPES_FOR_MIRRORING:
        raise ValueError(ERRORS["INVALID_COMMAND_ARG_VALUE"].format("entity_type", ", ".join(ENTITY_TYPES_FOR_MIRRORING)))

    # Get current integration context
    integration_context = get_integration_context()

    entity_type = "Accounts" if entity_type == "account" else "Hosts"
    # Initialize refetch_ids if it doesn't exist
    if entity_type not in integration_context:
        integration_context[entity_type] = {}

    if "refetch_ids" not in integration_context[entity_type]:
        integration_context[entity_type]["refetch_ids"] = []

    # Create the refetch entry
    refetch_entry = f"{entity_type}_{entity_id}"

    demisto.debug(f"Adding entity id to the integration context: {refetch_entry}")

    # Add to refetch list if not already present
    if refetch_entry not in integration_context[entity_type]["refetch_ids"]:
        integration_context[entity_type]["refetch_ids"].append(refetch_entry)

        # Update integration context
        set_integration_context(integration_context)

    demisto.debug(f"Updated {entity_type} ids list in the integration context: {integration_context[entity_type]['refetch_ids']}")


def get_valid_and_dropped_tags(tags: list[str]) -> tuple[list[str], list[str]]:
    """
    Return (valid_tags, dropped_tags) using TAG_REGEX.fullmatch().

    Note: does not strip/mutate inputs. If you want trimming, do it before calling.
    """
    valid: list[str] = []
    invalid: list[str] = []
    for t in tags:
        if TAGS_REGEX.fullmatch(t):
            valid.append(t)
        else:
            invalid.append(t)
    if invalid:
        demisto.debug(f"Dropping invalid tags which contains invalid characters: {invalid}")
    demisto.debug(f"Provided Valid tags(s): {valid}")
    return valid, invalid


class VectraException(Exception):
    """
    Custom Vectra Exception in case of Vectra API issue
    """


# ####               #### #
# ## COMMAND FUNCTIONS ## #
#                         #
def test_module(client: Client, integration_params: dict) -> str:
    """
    Tests API connectivity and authentication.

    Returning 'ok' indicates that the integration works like it is supposed to.
    Connection to the service is successful.
    Raises exceptions if something goes wrong.

    - params:
        - client: The API Client
        - integration_params: All additional integration settings
    - returns:
        'ok' if test passed, anything else if at least one test failed.
    """
    try:
        last_timestamp = None

        if integration_params.get("isFetch"):
            fetch_incidents(client, integration_params, True)
        else:
            # Client class should raise the exceptions, but if the test fails
            # the exception text is printed to the Cortex XSOAR UI.
            client.search_detections(max_results=1, last_timestamp=last_timestamp)
        message = "ok"

    except ValueError as e:
        message = str(e)
        demisto.debug(message)
    except DemistoException as e:
        if "Invalid token" in str(e):
            message = "Authorization Error: make sure API Token is properly set"
            demisto.debug(message)
        elif "Error occurred while creating an authorization token" in str(e):
            message = "Authorization Error: make sure Client ID and Client Secret are properly set"
            demisto.debug(message)
        elif "Verify that the server URL parameter is correct" in str(e):
            message = "Verify that the Vectra Server FQDN or IP is correct and that you have access to the server from your host"
            demisto.debug(message)
        else:
            raise e

    return message


def fetch_incidents(client: Client, integration_params: dict, is_test: bool = False):
    """
    Fetches incidents based on the provided client and integration parameters.

    :param client: The client object used to communicate with the Vectra API.
    :param integration_params: A dictionary containing integration parameters like fetch entity types and queries.
    :param is_test: A boolean indicating whether the command is being run in test mode.

    :return: A tuple containing the new last run details and a list of fetched incidents.
    """
    validate_fetch_incident_params(integration_params)
    fetch_entity_types = integration_params.get("fetch_entity_types", DEFAULT_FETCH_ENTITY_TYPES)
    look_back = integration_params.get("look_back", BACK_IN_TIME_SEARCH_IN_MINUTES)
    tags = argToList(integration_params.get("tags", ""))
    tags = [tag.strip() for tag in tags if tag.strip()]

    detection_category = integration_params.get("detection_category", "")
    detection_type = integration_params.get("detection_type", "").strip()
    create_multiple_incidents_by_timestamp = integration_params.get("create_multiple_incidents_by_timestamp", False)
    api_response: dict = {}

    # Get the last run and the last fetched value
    previous_last_run = get_last_run_details(integration_params, is_test)

    incidents = []
    new_last_run: dict = previous_last_run

    # We split the number of incidents to create into the number of remaining endpoints to call
    remaining_fetch_types: set = fetch_entity_types
    max_fetch = arg_to_number(integration_params.get("max_fetch", DEFAULT_MAX_FETCH))

    if max_fetch > MAX_RESULTS:  # type: ignore
        if is_test:
            raise ValueError(ERRORS["INVALID_MAX_FETCH"].format(max_fetch))
        demisto.debug(
            f"The value for the Max Fetch parameter is {max_fetch} which is greater than "
            f"{MAX_RESULTS}, so reducing it to {MAX_RESULTS}."
        )
        max_fetch = MAX_RESULTS

    max_created_incidents: int = max_fetch // len(remaining_fetch_types)  # type: ignore

    for entity_type in ENTITY_TYPES:
        entity_incidents: list = []
        if entity_type not in fetch_entity_types:
            continue

        last_fetched_timestamp = previous_last_run[entity_type]["last_timestamp"]
        last_fetched_id = previous_last_run[entity_type]["id"]
        # Forced to use "get" as this field wasn't present in the first version of this integration
        last_created_events = previous_last_run[entity_type].get("last_created_events", [])  # Retro-compat

        integration_context = get_integration_context()
        refetch_ids = integration_context.get(entity_type, {}).get("refetch_ids", [])

        # Remove refetch IDs from already_fetched and create the removed_ids list
        removed_ids = []
        for refetch_id in refetch_ids:
            if refetch_id in last_created_events:
                removed_ids.append(refetch_id)
                last_created_events.remove(refetch_id)

        demisto.debug(f"Removed closed {entity_type} ids from last run checkpoint: {removed_ids}")

        # Initialize entity_type in integration_context if it doesn't exist
        if entity_type not in integration_context:
            integration_context[entity_type] = {}

        # Update integration context with empty refetch_ids
        integration_context[entity_type]["refetch_ids"] = []

        # Set the updated integration context
        set_integration_context(integration_context)

        demisto.debug(f"{entity_type} - Last fetched incidentlast_timestamp : {last_fetched_timestamp} / ID : {last_fetched_id}")

        start_time = iso_date_to_vectra_start_time(last_fetched_timestamp, look_back)

        order_field = "last_detection_timestamp"
        last_detection_time, doc_modified_ts = start_time, None
        fetch_escalated_accounts_and_hosts = integration_params.get("fetch_escalated_accounts_and_hosts", False)
        if fetch_escalated_accounts_and_hosts:
            last_detection_time = None
            doc_modified_ts = start_time
            order_field = "_doc_modified_ts"

        new_last_run[entity_type] = previous_last_run.get(entity_type, {})
        new_last_run[entity_type]["last_created_events"] = last_created_events
        if entity_type == "Accounts":
            accounts_fetch_query = build_search_query_for_tags(
                integration_params.get("accounts_fetch_query", ""), "linked_account", tags
            )
            api_response = client.search_accounts(
                last_timestamp=last_detection_time,
                doc_modified_time=doc_modified_ts,
                search_query=accounts_fetch_query,
                order_field=order_field,
            )
        elif entity_type == "Hosts":
            hosts_fetch_query = build_search_query_for_tags(integration_params.get("hosts_fetch_query", ""), "host", tags)
            api_response = client.search_hosts(
                last_timestamp=last_detection_time,
                doc_modified_time=doc_modified_ts,
                search_query=hosts_fetch_query,
                order_field=order_field,
            )
        elif entity_type == "Detections":
            api_response = client.search_detections(
                last_timestamp=start_time,
                search_query=integration_params.get("detections_fetch_query"),  # type: ignore
            )

        if (api_response is None) or (api_response.get("count") is None):
            raise VectraException("API issue - Response is empty or invalid")

        if api_response.get("count") == 0:
            demisto.info(f"{entity_type} - No results")
        elif api_response.get("count", 0) > 0:
            demisto.debug(f"{entity_type} - {api_response.get('count')} objects fetched from Vectra")

            if api_response.get("results") is None:
                raise VectraException("API issue - Response is empty or invalid")

            # Due to backward search we need to avoid creating incidents of already ingested events
            api_results = api_response.get("results", [])
            ts_field = "last_detection_timestamp" if entity_type in ("Accounts", "Hosts") else "last_timestamp"
            for event in api_results:
                if len(entity_incidents) >= max_created_incidents:
                    demisto.debug(
                        f"{entity_type} - Maximum created incidents has been reached ({max_created_incidents})."
                        f" Skipping other objects."
                    )
                    break

                incident_uid = f"{entity_type}_{event.get('id')}"

                if (not create_multiple_incidents_by_timestamp) and incident_uid in last_created_events:
                    demisto.debug(
                        f"{entity_type} - Skipping object last_timestamp : {event.get('last_timestamp')} / ID : {event.get('id')}"
                    )
                    continue

                if create_multiple_incidents_by_timestamp:
                    event_id = str(event.get("id"))
                    event_ts = event.get(ts_field)
                    checkpoint_dict = new_last_run[entity_type].get("multiple_incidents_checkpoint", {}) or {}
                    if event_id in checkpoint_dict and checkpoint_dict.get(event_id) == event_ts:
                        continue

                incident_last_run = None
                if entity_type == "Accounts":
                    incident, incident_last_run = account_to_incident(
                        client, event, detection_category, detection_type, fetch_escalated_accounts_and_hosts
                    )
                elif entity_type == "Hosts":
                    incident, incident_last_run = host_to_incident(
                        client, event, detection_category, detection_type, fetch_escalated_accounts_and_hosts
                    )
                elif entity_type == "Detections":
                    incident, incident_last_run = detection_to_incident(event)
                else:
                    demisto.debug(f"The {entity_type=} didn't match any condition, can't create an incident, continue.")
                    continue

                # Search this incident in the last_run, if it's in, skip it, if not create it
                # Create incident UID and search for it
                if incident_last_run is not None:
                    demisto.debug(
                        f"{entity_type} - New incident from object "
                        f"last_timestamp : {incident_last_run.get('last_timestamp')} "
                        f"/ ID : {incident_last_run.get('id')}"
                    )
                    entity_incidents.append(incident)
                    new_last_run[entity_type]["last_timestamp"] = incident_last_run.get("last_timestamp")
                    new_last_run[entity_type]["id"] = incident_last_run.get("id")
                    existing_multiple_incidents_checkpoint = (
                        new_last_run[entity_type].get("multiple_incidents_checkpoint", {}) or {}
                    )
                    existing_multiple_incidents_checkpoint[str(incident_last_run.get("id"))] = event.get(ts_field)
                    new_last_run[entity_type]["multiple_incidents_checkpoint"] = existing_multiple_incidents_checkpoint
                    # We add this event in the list, as that's a new event we need to remember for next run
                    if incident_uid not in last_created_events:
                        last_created_events.append(incident_uid)

            if len(entity_incidents) > 0:
                demisto.info(f"{entity_type} - {len(entity_incidents)} incident(s) to create")
                incidents += entity_incidents
            else:
                demisto.debug(f"{entity_type} - No new incidents to create, keeping previous last_run data")
                new_last_run[entity_type] = previous_last_run[entity_type]

            # Update remaining list
            remaining_fetch_types.remove(entity_type)
            if len(remaining_fetch_types) > 0:
                max_created_incidents = (max_fetch - len(incidents)) // len(remaining_fetch_types)  # type: ignore

    if is_test:
        demisto.debug(f"Setting last run to : {new_last_run}")
        return previous_last_run, []

    demisto.info(f"{len(incidents)} total incident(s) to create.")

    return new_last_run, incidents


def get_modified_remote_data_command(client: Client) -> GetModifiedRemoteDataResponse:
    """
    Get modified remote data from the Vectra platform and prepare it for mirroring in XSOAR.

    Args:
        client (Client): An instance of the VectraClient class.

    Returns:
        GetModifiedRemoteDataResponse: List of incidents IDs which are modified since the last update.
    """
    args = demisto.args()
    command_args = GetModifiedRemoteDataArgs(args)
    command_last_run_date = dateparser.parse(
        command_args.last_update,
        settings={"TIMEZONE": "UTC"},
    ).strftime("%Y-%m-%dT%H%M")  # type: ignore
    modified_entities_ids = []

    demisto.debug(f"Last update date of get-modified-remote-data command is {command_last_run_date}.")

    for entity_type in ENTITY_TYPES_FOR_MIRRORING:
        entity_next_url = None
        page = 1
        page_size = 500
        while True:
            if entity_next_url:
                # Parse the URL
                parsed_url = urlparse(entity_next_url)
                # Extract the query parameters
                query_params = parse_qs(parsed_url.query)
                page = arg_to_number(query_params.get("page", [""])[0], arg_name="page")  # type: ignore
                page_size = arg_to_number(query_params.get("page_size", [""])[0], arg_name="page_size")  # type: ignore
                query_string = query_params.get("query_string", [""])[0]  # type: ignore
            else:
                query_string = "_doc_modified_ts:>=" + command_last_run_date

            if entity_type == "host":
                response = client.search_hosts(search_query_only=query_string, page=page, max_results=page_size)
            else:
                response = client.search_accounts(search_query_only=query_string, page=page, max_results=page_size)

            entities = response.get("results", [])
            entity_next_url = response.get("next")
            if len(entities) == 0:
                break
            # Extra ID and type of the entities
            modified_entities_ids.extend([str(entity.get("id")) + f"-{entity_type}" for entity in entities])

            # Mirroring limit
            if len(modified_entities_ids) > MAX_MIRRORING_LIMIT / 2:
                demisto.debug(f"Max mirroring limit reached for {entity_type}.")
                break

            # If there is no data on the next page
            if not entity_next_url:
                break

    # Filter out None values if there are any.
    modified_entities_ids: list[str] = list(filter(None, modified_entities_ids))  # type: ignore
    demisto.debug(
        f"Performing get-modified-remote-data command. Numbers Entity IDs to update in XSOAR: {len(modified_entities_ids)}"
    )
    demisto.debug(f"Performing get-modified-remote-data command. Entity IDs to update in XSOAR: {modified_entities_ids}")

    # Filter out any duplicate incident IDs.
    updated_incident_ids = list(set(modified_entities_ids))

    # At max 5,000 incidents should be updated.
    updated_incident_ids = updated_incident_ids[:5000]

    return GetModifiedRemoteDataResponse(modified_incident_ids=updated_incident_ids)


def get_remote_data_command(client: Client, integration_params: dict = {}) -> GetRemoteDataResponse:
    """
    Get remote data for a specific entity from the Vectra platform and prepare it for mirroring in XSOAR.

    Args:
        client (Client): An instance of the VectraClient class.
        integration_params (Dict): The integration parameters.

    Returns:
        GetRemoteDataResponse: An object containing the remote incident data and any new entries to return to XSOAR.
    """
    detection_category = integration_params.get("detection_category", "")
    detection_type = integration_params.get("detection_type", "").strip()
    refetch_closed_incidents = argToBoolean(integration_params.get("refetch_closed_incidents", False))
    new_entries_to_return: list[dict] = []

    args = demisto.args()
    dbot_mirror_id: str = args.get("id")  # type: ignore
    demisto.debug(f"dbot_mirror_id:{dbot_mirror_id}")
    entity_id_type = dbot_mirror_id.split("-")
    vectra_entity_id = entity_id_type[0] if entity_id_type else ""
    vectra_entity_type = entity_id_type[1] if entity_id_type else ""

    demisto.debug(f"vectra_entity_id:{vectra_entity_type}")
    demisto.debug(f"Getting update for remote {vectra_entity_id}.")

    command_last_run_dt = arg_to_datetime(args.get("lastUpdate"), arg_name="lastUpdate", required=True)
    command_last_run_timestamp = command_last_run_dt.strftime(DATE_FORMAT)  # type: ignore

    demisto.debug(
        f"The time when the last time get-remote-data command is called for current incident is {command_last_run_timestamp}."
    )

    # Retrieve the latest entity data from the Vectra platform.
    if vectra_entity_type == "account":
        remote_incident_data = client.get_account_by_account_id(account_id=vectra_entity_id)
        groups_response = client.list_group_request(
            group_type="account",
            account_names=[remote_incident_data.get("name")],
        )  # type: ignore
        remote_incident_data.update({"groups": groups_response.get("results", [])})
    else:
        remote_incident_data = client.get_host_by_host_id(host_id=vectra_entity_id)

    if not remote_incident_data:
        return "Incident was not found."  # type: ignore

    remote_incident_data["_vectra_specific"] = {
        "entity_type": vectra_entity_type,
        "UI_URL": forge_entity_url(vectra_entity_type, remote_incident_data.get("id")),
    }

    updated_severity_score = severity_string_to_int(
        scores_to_severity(remote_incident_data.get("threat"), remote_incident_data.get("certainty"))
    )

    remote_incident_data.update({"calculated_severity": updated_severity_score})

    # Get detection set.
    detection_set = remote_incident_data.get("detection_set", [])

    # Collect the detections if the detection set is not empty.
    detections = []
    if len(detection_set) != 0:
        if vectra_entity_type == "account":
            search_query = build_search_query_for_detections(vectra_entity_id, "account", detection_category, detection_type)
            api_response = client.search_detections(state="active", search_query=search_query)
            if (api_response is None) or (api_response.get("count") is None):
                raise VectraException("API issue - Response is empty or invalid")

            if api_response.get("count") == 0:
                demisto.info(f"Found 0 detection(s) for the account with the ID: {vectra_entity_id}.")
            elif api_response.get("count", 0) > 0:
                demisto.info(f"Found {api_response.get('count')} detection(s) for the account with the ID: {vectra_entity_id}.")

                if api_response.get("results") is None:
                    raise VectraException("API issue - Response is empty or invalid")

                # Due to backward search we need to avoid creating incidents of already ingested events
                detections = api_response.get("results", [])
                for detection in detections:
                    detection["url"] = forge_entity_url("detection", detection.get("id"))
        else:
            detections_data = client.list_detections_by_host_id(vectra_entity_id, "active", detection_category, detection_type)
            detections = detections_data.get("results", [])
            for detection in detections:
                detection["url"] = forge_entity_url("detection", detection.get("id"))
            demisto.debug(f"Found {len(detections)} detection(s) for the host with the ID: {vectra_entity_id}.")

    # Add detection details to the entity.
    remote_incident_data.update({"detection_details": detections})

    assignment_details = remote_incident_data.get("assignment", {})

    if not assignment_details:
        past_assignments = remote_incident_data.get("past_assignments", [{}])
        if past_assignments and isinstance(past_assignments, list):
            assignment_details = past_assignments[0]

    if assignment_details:
        if not assignment_details.get("resolved_by"):
            assignment_details["resolved_by"] = {"username": ""}
            assignment_details["outcome"] = {"title": ""}
            assignment_details["date_resolved"] = ""
    else:
        assignment_details = EMPTY_ASSIGNMENT

    remote_incident_data.update({"assignment_details": assignment_details})

    if detections and not refetch_closed_incidents:
        reopen_in_xsoar(new_entries_to_return, entity_id_type)

    notes = remote_incident_data.get("notes", [])

    new_entry_notes = add_notes_to_new_entries(notes, command_last_run_dt)
    new_entries_to_return.extend(new_entry_notes)

    demisto.debug(f"remote_incident_data:{remote_incident_data}")
    return GetRemoteDataResponse(remote_incident_data, new_entries_to_return)


def update_remote_system_command(client: Client) -> str:
    """
    Update a remote system based on changes in the XSOAR incident.

    Args:
        client (Client): An instance of the VectraClient class.

    Returns:
        str: The ID of the updated remote entity.
    """
    args = demisto.args()
    parsed_args = UpdateRemoteSystemArgs(args)
    # Get remote incident ID
    remote_entity_id = parsed_args.remote_incident_id
    demisto.debug(f"Remote Incident ID: {remote_entity_id}")
    delta = parsed_args.delta or {}
    demisto.debug(f"Delta: {delta}")
    # Get XSOAR incident id
    data = parsed_args.data or {}
    xsoar_incident_id = data.get("id", "")
    demisto.debug(f"XSOAR Incident ID: {xsoar_incident_id}")
    new_entries = parsed_args.entries or []
    xsoar_tags: list = delta.get("tags") or []
    mirror_entity_id = remote_entity_id.split("-")[0]
    remote_entity_type = remote_entity_id.split("-")[1]

    params = demisto.params()
    refetch_closed_incidents = argToBoolean(params.get("refetch_closed_incidents", False))

    # For notes
    if new_entries:
        for entry in new_entries:
            entry_id = entry.get("id")
            demisto.debug(f"Sending the entry with ID: {entry_id} and Type: {entry.get('type')}")
            # Get note content and user
            entry_content = re.sub(r"([^\n])\n", r"\1\n\n", entry.get("contents", ""))
            entry_user = entry.get("user", "dbot") or "dbot"

            note_str = (
                f"[Mirrored From XSOAR] XSOAR Incident ID: {xsoar_incident_id}\n\nNote: {entry_content}\n\nAdded By: {entry_user}"
            )
            # API request for adding notes
            client.add_note_request(entity_id=mirror_entity_id, entity_type=remote_entity_type, note=note_str)

    # For tags
    res = client.list_entity_tags_request(entity_id=mirror_entity_id, entity_type=remote_entity_type)
    vectra_tags = res.get("tags") or []
    if xsoar_tags or (not xsoar_tags and vectra_tags and "tags" in delta):
        demisto.debug(f"Sending the tags: {xsoar_tags}")
        # Drop invalid tags that do not fully match the allowed pattern
        if xsoar_tags:
            valid_tags, dropped_tags = get_valid_and_dropped_tags(xsoar_tags)
            xsoar_tags = valid_tags
        client.update_entity_tags_request(entity_id=mirror_entity_id, entity_type=remote_entity_type, tag_list=xsoar_tags)

    # For closing notes if the XSOAR incident is closed.
    send_close_notes(
        client,
        data,
        xsoar_incident_id,
        remote_entity_id,
        remote_entity_type,
        mirror_entity_id,
        delta,
        refetch_closed_incidents,
        parsed_args,
    )

    return remote_entity_id


def send_close_notes(
    client: Client,
    data: dict,
    xsoar_incident_id: str,
    remote_entity_id: str,
    remote_entity_type: str,
    mirror_entity_id: int,
    delta: dict,
    refetch_closed_incidents: bool,
    parsed_args: UpdateRemoteSystemArgs,
):
    """
    Send close notes to Vectra and also remove the assignment when the XSOAR incident is reopened.

    Args:
        data (dict): A dictionary of data from Vectra.
        xsoar_incident_id (str): The ID of the XSOAR incident.
        delta (dict): A dictionary of changes in the XSOAR incident.
        refetch_closed_incidents (bool): A boolean to determine whether to refetch closed incidents.
    """
    delta_keys = delta.keys()
    closing_user_id = delta.get("closingUserId")
    if "closingUserId" in delta_keys and parsed_args.incident_changed:
        # For Closing notes
        if parsed_args.inc_status == IncidentStatus.DONE:
            if refetch_closed_incidents:
                add_refetch_id_to_integration_context(str(mirror_entity_id), remote_entity_type)

            close_notes = data.get("closeNotes", "")
            close_reason = data.get("closeReason", "")
            close_user_id = data.get("closingUserId", "")

            closing_note = (
                f"[Mirrored From XSOAR] XSOAR Incident ID: {xsoar_incident_id}\n\n"
                f"Close Reason: {close_reason}\n\n"
                f"Closed By: {close_user_id}\n\n"
                f"Close Notes: {close_notes}"
            )
            demisto.debug(f"Closing Comment: {closing_note}")
            client.add_note_request(entity_id=mirror_entity_id, entity_type=remote_entity_type, note=closing_note)
        # Remove assignment in Vectra if incident is reopened.
        elif parsed_args.inc_status == IncidentStatus.ACTIVE and closing_user_id == "":
            api_response = {}
            if "account" in remote_entity_type:
                api_response = client.list_assignments_request(accounts=mirror_entity_id, page_size=1)  # type: ignore
            elif "host" in remote_entity_type:
                api_response = client.list_assignments_request(hosts=mirror_entity_id, page_size=1)  # type: ignore

            assignment_details = api_response.get("results", [])
            assignment = assignment_details[0] if assignment_details else {}
            if assignment:
                assignment_id = assignment.get("id", "")
                if not assignment.get("resolved_by"):
                    demisto.debug(
                        f"Removing assignment with the ID: {assignment_id} for the incident having"
                        f" remote entity ID: {remote_entity_id} as incident in XSOAR is reopened."
                    )
                    client.delete_assignment(assignment_id)


def vectra_search_accounts_command(client: Client, **kwargs) -> CommandResults:
    """
    Returns several Account objects matching the search criteria passed as arguments

    - params:
        - client: Vectra Client
        - kwargs: The different possible search query arguments
    - returns
        CommandResults to be used in War Room
    """
    api_response = client.search_accounts(**kwargs)

    count = api_response.get("count")
    if count is None:
        raise VectraException("API issue - Response is empty or invalid")

    accounts_data = []
    if count == 0:
        readable_output = "Cannot find any Account."
    else:
        if api_response.get("results") is None:
            raise VectraException("API issue - Response is empty or invalid")

        api_results = api_response.get("results", [])

        for account in api_results:
            accounts_data.append(extract_account_data(account))

        readable_output_keys = ["ID", "Username", "Severity", "URL"]
        readable_output = tableToMarkdown(
            name=f"Accounts table (Showing max {MAX_RESULTS} entries)",
            t=accounts_data,
            headers=readable_output_keys,
            url_keys=["URL"],
            date_fields=["AssignedDate", "LastDetectionTimestamp"],
        )

    command_result = CommandResults(
        readable_output=readable_output,
        outputs_prefix="Vectra.Account",
        outputs_key_field="ID",
        outputs=accounts_data,
        raw_response=api_response,
    )

    return command_result


def vectra_search_detections_command(client: Client, **kwargs) -> CommandResults:
    """
    Returns several Detection objects matching the search criteria passed as arguments

    - params:
        - client: Vectra Client
        - kwargs: The different possible search query arguments
    - returns
        CommandResults to be used in War Room
    """
    api_response = client.search_detections(**kwargs)

    count = api_response.get("count")
    if count is None:
        raise VectraException("API issue - Response is empty or invalid")

    detections_data = []
    if count == 0:
        readable_output = "Cannot find any Detection."
    else:
        if api_response.get("results") is None:
            raise VectraException("API issue - Response is empty or invalid")

        api_results = api_response.get("results", [])

        # Define which fields we want to exclude from the context output
        # detection_context_excluded_fields = []
        # Context Keys
        # context_keys = []

        for detection in api_results:
            detection_data = extract_detection_data(detection)
            # detection_data = {k: detection_data[k] for k in detection_data if k not in detection_context_excluded_fields}
            detections_data.append(detection_data)

        readable_output_keys = ["ID", "Name", "Severity", "LastTimestamp", "Category", "URL"]
        readable_output = tableToMarkdown(
            name=f"Detections table (Showing max {MAX_RESULTS} entries)",
            t=detections_data,
            headers=readable_output_keys,
            url_keys=["URL"],
            date_fields=["AssignedDate", "FirstTimestamp", "LastTimestamp"],
        )

    command_result = CommandResults(
        readable_output=readable_output,
        outputs_prefix="Vectra.Detection",
        outputs_key_field="ID",
        outputs=detections_data,
        raw_response=api_response,
    )

    return command_result


def vectra_search_hosts_command(client: Client, **kwargs) -> CommandResults:
    """
    Returns several Host objects matching the search criteria passed as arguments

    - params:
        - client: Vectra Client
        - kwargs: The different possible search query arguments
    - returns
        CommandResults to be used in War Room
    """
    api_response = client.search_hosts(**kwargs)

    count = api_response.get("count")
    if count is None:
        raise VectraException("API issue - Response is empty or invalid")

    hosts_data = []
    if count == 0:
        readable_output = "Cannot find any Host."
    else:
        if api_response.get("results") is None:
            raise VectraException("API issue - Response is empty or invalid")

        api_results = api_response.get("results", [])

        for host in api_results:
            hosts_data.append(extract_host_data(host))

        readable_output_keys = ["ID", "Hostname", "Severity", "LastDetectionTimestamp", "URL"]
        readable_output = tableToMarkdown(
            name=f"Hosts table (Showing max {MAX_RESULTS} entries)",
            t=hosts_data,
            headers=readable_output_keys,
            url_keys=["URL"],
            date_fields=["AssignedDate", "LastDetectionTimestamp"],
        )

    command_result = CommandResults(
        readable_output=readable_output,
        outputs_prefix="Vectra.Host",
        outputs_key_field="ID",
        outputs=hosts_data,
        raw_response=api_response,
    )

    return command_result


def vectra_search_assignments_command(client: Client, **kwargs) -> CommandResults:
    """
    Returns several Assignment objects matching the search criteria passed as arguments

    - params:
        - client: Vectra Client
        - kwargs: The different possible search query arguments
    - returns
        CommandResults to be used in War Room
    """
    api_response = client.search_assignments(**kwargs)

    count = api_response.get("count")
    if count is None:
        raise VectraException("API issue - Response is empty or invalid")

    assignments_data = []
    if count == 0:
        readable_output = "Cannot find any Assignments."
    else:
        if api_response.get("results") is None:
            raise VectraException("API issue - Response is empty or invalid")

        api_results = api_response.get("results", [])

        for assignment in api_results:
            assignments_data.append(extract_assignment_data(assignment))

        readable_output_keys = ["ID", "IsResolved", "AssignedTo", "AccountID", "HostID"]
        readable_output = tableToMarkdown(
            name=f"Assignments table (Showing max {MAX_RESULTS} entries)",
            t=assignments_data,
            headers=readable_output_keys,
            date_fields=["AssignedDate", "ResolvedDate"],
        )

    command_result = CommandResults(
        readable_output=readable_output,
        outputs_prefix="Vectra.Assignment",
        outputs_key_field="ID",
        outputs=assignments_data,
        raw_response=api_response,
    )

    return command_result


def vectra_search_outcomes_command(client: Client, **kwargs) -> CommandResults:
    """
    Returns several Assignment outcome objects matching the search criteria passed as arguments

    - params:
        - client: Vectra Client
        - kwargs: The different possible search query arguments
    - returns
        CommandResults to be used in War Room
    """
    api_response = client.search_outcomes(**kwargs)

    count = api_response.get("count")
    if count is None:
        raise VectraException("API issue - Response is empty or invalid")

    outcomes_data = []
    if count == 0:
        readable_output = "Cannot find any Outcomes."
    else:
        if api_response.get("results") is None:
            raise VectraException("API issue - Response is empty or invalid")

        api_results = api_response.get("results", [])

        for outcome in api_results:
            outcomes_data.append(extract_outcome_data(outcome))

        readable_output_keys = ["ID", "Title", "Category", "IsBuiltIn"]
        readable_output = tableToMarkdown(
            name=f"Outcomes table (Showing max {MAX_RESULTS} entries)", t=outcomes_data, headers=readable_output_keys
        )

    command_result = CommandResults(
        readable_output=readable_output,
        outputs_prefix="Vectra.Outcome",
        outputs_key_field="ID",
        outputs=outcomes_data,
        raw_response=api_response,
    )

    return command_result


def vectra_search_users_command(client: Client, **kwargs) -> CommandResults:
    """
    Returns several Vectra Users objects matching the search criteria passed as arguments

    - params:
        - client: Vectra Client
        - kwargs: The different possible search query arguments
    - returns
        CommandResults to be used in War Room
    """
    api_response = client.search_users(**kwargs)

    count = api_response.get("count")
    if count is None:
        raise VectraException("API issue - Response is empty or invalid")

    users_data = []
    if count == 0:
        readable_output = "Cannot find any Vectra Users."
    else:
        if api_response.get("results") is None:
            raise VectraException("API issue - Response is empty or invalid")

        api_results = api_response.get("results", [])

        for assignment in api_results:
            users_data.append(extract_user_data(assignment))

        readable_output_keys = ["ID", "Role", "Type", "Username", "LastLoginDate"]
        readable_output = tableToMarkdown(
            name=f"Vectra Users table (Showing max {MAX_RESULTS} entries)",
            t=users_data,
            headers=readable_output_keys,
            date_fields=["LastLoginDate"],
        )

    command_result = CommandResults(
        readable_output=readable_output,
        outputs_prefix="Vectra.User",
        outputs_key_field="ID",
        outputs=users_data,
        raw_response=api_response,
    )

    return command_result


def vectra_get_account_by_id_command(client: Client, id: str) -> CommandResults:
    """
    Gets Account details using its ID

    - params:
        - client: Vectra Client
        - id: The Account ID
    - returns
        CommandResults to be used in War Room
    """
    # Check args
    if not id:
        raise VectraException('"id" not specified')

    search_query: str = f"account.id:{id}"

    api_response = client.search_accounts(search_query_only=search_query)

    count = api_response.get("count")
    if count is None:
        raise VectraException("API issue - Response is empty or invalid")
    if count > 1:
        raise VectraException("Multiple Accounts found")

    account_data = None
    if count == 0:
        readable_output = f'Cannot find Account with ID "{id}".'
    else:
        if api_response.get("results") is None:
            raise VectraException("API issue - Response is empty or invalid")

        api_results = api_response.get("results", [])
        account_data = extract_account_data(api_results[0])

        readable_output = tableToMarkdown(
            name=f"Account ID {id} details table", t=account_data, url_keys=["URL"], date_fields=["LastDetectionTimestamp"]
        )

    command_result = CommandResults(
        readable_output=readable_output,
        outputs_prefix="Vectra.Account",
        outputs_key_field="ID",
        outputs=account_data,
        raw_response=api_response,
    )

    return command_result


def vectra_get_detection_by_id_command(client: Client, id: str) -> CommandResults:
    """
    Gets Detection details using its ID

    - params:
        - client: Vectra Client
        - id: The Detection ID
    - returns
        CommandResults to be used in War Room
    """
    # Check args
    if not id:
        raise VectraException('"id" not specified')

    search_query: str = f"detection.id:{id}"

    api_response = client.search_detections(search_query_only=search_query)

    count = api_response.get("count")
    if count is None:
        raise VectraException("API issue - Response is empty or invalid")
    if count > 1:
        raise VectraException("Multiple Detections found")

    detection_data = None
    if count == 0:
        readable_output = f'Cannot find Detection with ID "{id}".'
    else:
        if api_response.get("results") is None:
            raise VectraException("API issue - Response is empty or invalid")

        api_results = api_response.get("results", [])
        detection_data = extract_detection_data(api_results[0])

        readable_output = tableToMarkdown(
            name=f"Detection ID '{id}' details table",
            t=detection_data,
            url_keys=["URL"],
            date_fields=["FirstTimestamp", "LastTimestamp"],
        )

    command_result = CommandResults(
        readable_output=readable_output,
        outputs_prefix="Vectra.Detection",
        outputs_key_field="ID",
        outputs=detection_data,
        raw_response=api_response,
    )

    return command_result


def vectra_get_host_by_id_command(client: Client, id: str) -> CommandResults:
    """
    Gets Host details using its ID

    - params:
        - client: Vectra Client
        - id: The Host ID
    - returns
        CommandResults to be used in War Room
    """
    # Check args
    if not id:
        raise VectraException('"id" not specified')

    search_query: str = f"host.id:{id}"

    api_response = client.search_hosts(search_query_only=search_query)

    count = api_response.get("count")
    if count is None:
        raise VectraException("API issue - Response is empty or invalid")
    if count > 1:
        raise VectraException("Multiple Hosts found")

    host_data = None
    if count == 0:
        readable_output = f'Cannot find Host with ID "{id}".'
    else:
        if api_response.get("results") is None:
            raise VectraException("API issue - Response is empty or invalid")

        api_results = api_response.get("results", [])
        host_data = extract_host_data(api_results[0])

        readable_output = tableToMarkdown(
            name=f"Host ID {id} details table",
            t=host_data,
            url_keys=["URL"],
            date_fields=["LastDetectionTimestamp"],
        )

    command_result = CommandResults(
        readable_output=readable_output,
        outputs_prefix="Vectra.Host",
        outputs_key_field="ID",
        outputs=host_data,
        raw_response=api_response,
    )

    return command_result


def get_detection_pcap_file_command(client: Client, id: str):
    """
    Downloads a PCAP fileassociated to a detection

    - params:
        - client: Vectra Client
        - id: The Detection ID
    - returns:
        A commandResult to use in the War Room
    """
    if not id:
        raise VectraException('"id" not specified')

    api_response = client.get_pcap_by_detection_id(id=id)

    # 404 API error will be raised by the Client class
    filename = f"detection-{id}.pcap"
    file_content = api_response.content
    pcap_file = fileResult(filename, file_content)

    return pcap_file


def mark_detection_as_fixed_command(client: Client, id: str, fixed: str) -> CommandResults:
    """
    Toggles a detection status as : fixed / Not fixed

    - params:
        - client: Vectra Client
        - id: The Detection ID
        - fixed: The Detection future state
    """

    if (id is None) or (id == ""):
        raise VectraException('"id" not specified')
    fixed_as_bool = str2bool(fixed)
    if fixed_as_bool is None:
        raise VectraException('"fixed" not specified')

    api_response = client.markasfixed_by_detection_id(id=id, fixed=fixed_as_bool)

    # 404 API error will be raised by the Client class
    command_result = CommandResults(
        readable_output=f'Detection "{id}" successfully {"marked" if fixed_as_bool else "unmarked"} as fixed.',
        raw_response=api_response,
    )

    return command_result


def vectra_get_assignment_by_id_command(client: Client, id: str) -> CommandResults:
    """
    GetsAssignment details using its ID

    - params:
        - client: Vectra Client
        - id: The Assignment ID
    - returns
        CommandResults to be used in War Room
    """
    # Check args
    if not id:
        raise VectraException('"id" not specified')

    api_response = client.search_assignments(id=id)

    assignment_data = None
    # Assignment doesn't follow classic describe behavior
    obtained_assignment = api_response.get("assignment")
    if obtained_assignment is None:
        readable_output = f'Cannot find Assignment with ID "{id}".'
    else:
        assignment_data = extract_assignment_data(obtained_assignment)

        readable_output = tableToMarkdown(
            name=f"Assignment ID {id} details table", t=assignment_data, date_fields=["AssignedDate", "ResolvedDate"]
        )

    command_result = CommandResults(
        readable_output=readable_output,
        outputs_prefix="Vectra.Assignment",
        outputs_key_field="ID",
        outputs=assignment_data,
        raw_response=api_response,
    )

    return command_result


def vectra_assignment_assign_command(
    client: Client,
    assignee_id: str = None,  # type: ignore
    account_id: str = None,
    host_id: str = None,  # type: ignore
    assignment_id: str = None,
) -> CommandResults:  # type: ignore
    """
    Assign or reassign an Account/Host

    - params:
        - client: Vectra Client
        - assignee_id: The Vectra User ID who want to assign to
        - account_id: The Account ID
        - host_id: The Host ID
        - assignment_id: The existing assignment ID associated with the targeted Entity, if there is any
    - returns
        CommandResults to be used in War Room
    """
    # Check args
    if not assignee_id:
        raise VectraException('"assignee_id" not specified')
    if (
        ((assignment_id is None) and (account_id is None) and (host_id is None))
        or (account_id and host_id)
        or (assignment_id and (account_id or host_id))
    ):
        raise VectraException('You must specify one of "assignment_id", "account_id" or "host_id"')
    if assignment_id is None:
        api_response = client.update_assignment(assignee_id=assignee_id, account_id=account_id, host_id=host_id)
    else:
        api_response = client.update_assignment(assignee_id=assignee_id, assignment_id=assignment_id)

    # 40x API error will be raised by the Client class
    obtained_assignment = api_response.get("assignment")
    assignment_data = extract_assignment_data(obtained_assignment)

    readable_output = tableToMarkdown(name="Assignment details table", t=assignment_data)

    command_result = CommandResults(
        readable_output=readable_output,
        outputs_prefix="Vectra.Assignment",
        outputs_key_field="ID",
        outputs=assignment_data,
        raw_response=api_response,
    )

    return command_result


def vectra_assignment_resolve_command(
    client: Client,
    assignment_id: str = None,
    outcome_id: str = None,  # type: ignore
    note: str = None,
    detections_filter: str = None,  # type: ignore
    filter_rule_name: str = None,
    detections_list: str = None,
):  # type: ignore
    """
    Resolve an existing assignment

    - params:
        - client: Vectra Client
        - assignment_id: Assignment ID
        - outcome_id: The Outcome ID
        - detections_filter: Filter mode to use ('None' or 'Filter Rule') [Default: None]
        - filter_rule_name: Filter rule name (when detections_filter equals 'Filter Rule')
        - detections_list: List of the Detections to filter
    - returns
        CommandResults to be used in War Room
    """
    # Check args
    if not assignment_id:
        raise VectraException('"assignment_id" not specified')
    if not outcome_id:
        raise VectraException('"outcome_id" not specified')

    if detections_filter == "Filter Rule":
        if not filter_rule_name:
            raise VectraException('"filter_rule_name" not specified')
        if not detections_list:
            raise VectraException('"detections_list" not specified')
        api_response = client.resolve_assignment(
            assignment_id=assignment_id,
            outcome_id=outcome_id,
            note=note,
            rule_name=filter_rule_name,
            detections_list=detections_list,
        )
    else:
        api_response = client.resolve_assignment(assignment_id=assignment_id, outcome_id=outcome_id, note=note)

    # 40x API error will be raised by the Client class
    obtained_assignment = api_response.get("assignment")
    assignment_data = extract_assignment_data(obtained_assignment)

    readable_output = tableToMarkdown(name="Assignment details table", t=assignment_data)

    command_result = CommandResults(
        readable_output=readable_output,
        outputs_prefix="Vectra.Assignment",
        outputs_key_field="ID",
        outputs=assignment_data,
        raw_response=api_response,
    )

    return command_result


def vectra_get_outcome_by_id_command(client: Client, id: str) -> CommandResults:
    """
    Gets Outcome details using its ID

    - params:
        - client: Vectra Client
        - id: The Outcome ID
    - returns
        CommandResults to be used in War Room
    """
    # Check args
    if not id:
        raise VectraException('"id" not specified')

    api_response = client.search_outcomes(id=id)

    outcome_data = None
    obtained_id = api_response.get("id")
    if obtained_id is None:
        readable_output = f'Cannot find Outcome with ID "{id}".'
    else:
        outcome_data = extract_outcome_data(api_response)

        readable_output = tableToMarkdown(name=f"Outcome ID {id} details table", t=outcome_data)

    command_result = CommandResults(
        readable_output=readable_output,
        outputs_prefix="Vectra.Outcome",
        outputs_key_field="ID",
        outputs=outcome_data,
        raw_response=api_response,
    )

    return command_result


def vectra_outcome_create_command(client: Client, category: str, title: str) -> CommandResults:
    """
    Creates a new Outcome

    - params:
        - client: Vectra Client
        - category: The Outcome category (one of "BTP,MTP,FP")
        - title: A custom title for this new outcome
    - returns
        CommandResults to be used in War Room
    """
    # Check args
    if not category:
        raise VectraException('"category" not specified')
    if not title:
        raise VectraException('"title" not specified')

    api_response = client.create_outcome(category=category, title=title)

    # 40x API error will be raised by the Client class
    outcome_data = extract_outcome_data(api_response)

    readable_output = tableToMarkdown(name="Newly created Outcome details table", t=outcome_data)

    command_result = CommandResults(
        readable_output=readable_output,
        outputs_prefix="Vectra.Outcome",
        outputs_key_field="ID",
        outputs=outcome_data,
        raw_response=api_response,
    )

    return command_result


def vectra_get_user_by_id_command(client: Client, id: str) -> CommandResults:
    """
    Gets Vectra User details using its ID

    - params:
        - client: Vectra Client
        - id: The User ID
    - returns
        CommandResults to be used in War Room
    """
    # Check args
    if not id:
        raise VectraException('"id" not specified')

    api_response = client.search_users(id=id)

    user_data = None
    obtained_id = api_response.get("id")
    if obtained_id is None:
        readable_output = f'Cannot find Vectra User with ID "{id}".'
    else:
        user_data = extract_user_data(api_response)

        readable_output = tableToMarkdown(name=f"Vectra User ID {id} details table", t=user_data, date_fields=["LastLoginDate"])

    command_result = CommandResults(
        readable_output=readable_output,
        outputs_prefix="Vectra.User",
        outputs_key_field="ID",
        outputs=user_data,
        raw_response=api_response,
    )

    return command_result


def add_tags_command(client: Client, type: str, id: str, tags: str) -> CommandResults:
    """
    Adds several tags to an account/host/detection

    - params:
        - client: Vectra Client
        - type: The object to work with ("account", "host" or "detection")
        - id: The id ID the account/host/detection
        - tags: The tags list (comma separated)
    """

    if not type:
        raise VectraException('"type" not specified')
    if not id:
        raise VectraException('"id" not specified')
    if not tags:
        raise VectraException('"tags" not specified')

    api_response = client.add_tags(id=id, type=type, tags=tags.split(","))

    # 404 API error will be raised by the Client class
    command_result = CommandResults(readable_output=f'Tags "{tags}" successfully added.', raw_response=api_response)

    return command_result


def del_tags_command(client: Client, type: str, id: str, tags: str) -> CommandResults:
    """
    Removes several tags from an account/host/detection

    - params:
        - client: Vectra Client
        - type: The object to work with ("account", "host" or "detection")
        - id: The ID of the account/host/detection
        - tags: The tags list (comma separated)
    """

    if not type:
        raise VectraException('"type" not specified')
    if not id:
        raise VectraException('"id" not specified')
    if not tags:
        raise VectraException('"tags" not specified')

    api_response = client.del_tags(id=id, type=type, tags=tags.split(","))

    # 404 API error will be raised by the Client class
    command_result = CommandResults(readable_output=f'Tags "{tags}" successfully deleted.', raw_response=api_response)

    return command_result


def tag_list_command(client: Client, entity_type: str, args: dict) -> CommandResults:
    """
    List all tags of specific Account, Host or Detection.

    Args:
        client : An instance of the Client class.
        entity_type: The type Account, Host or Detection
        args: dictionary of arguments.
    Returns:
        CommandResults: The command results containing the outputs, readable output, raw response, and outputs key field.
    """
    entity_id = arg_to_number(args.get("id"), arg_name="id", required=True)

    validate_positive_integer_arg(entity_id, arg_name="id", required=True)

    existing_tag_res = client.list_tags_request(entity_id=entity_id, entity_type=entity_type)  # type: ignore
    raw_res = deepcopy(existing_tag_res)
    existing_tag_res_status = existing_tag_res.get("status", "")
    if (
        not existing_tag_res_status
        or not isinstance(existing_tag_res_status, str)
        or existing_tag_res_status.lower() != "success"
    ):
        message = "Something went wrong."
        if existing_tag_res.get("message"):
            message += f" Message: {existing_tag_res.get('message')}."
        raise VectraException(message)
    tags_resp = existing_tag_res.get("tags", [])

    human_readable = f"##### No tags were found for the given {entity_type} ID."
    if tags_resp and isinstance(tags_resp, list):
        tags_resp = [tag.strip() for tag in tags_resp if isinstance(tag, str) and tag.strip()]
        if tags_resp:
            tags_resp = f"**{'**, **'.join(tags_resp)}**"
            human_readable = f"##### List of tags: {tags_resp}"

    existing_tag_res["ID"] = entity_id
    existing_tag_res["Tags"] = existing_tag_res["tags"]
    del existing_tag_res["status"]
    del existing_tag_res["tags"]
    del existing_tag_res["tag_id"]
    return CommandResults(
        outputs_prefix=f"Vectra.{entity_type.capitalize()}",
        outputs=createContext(remove_empty_elements(existing_tag_res)),
        readable_output=human_readable,
        raw_response=raw_res,
        outputs_key_field="ID",
    )


def note_add_command(client: Client, entity_type: str, args: dict) -> CommandResults:
    """
    Adds a note to an account/host/detection in Vectra API.

    Args:
        client : An instance of the Client class.
        entity_type (str): The type of the object (account, host or detection).

    Returns:
        CommandResults: The command results containing the outputs, readable output, raw response, and outputs key field.
    """
    entity_id = arg_to_number(args.get(f"{entity_type}_id"), arg_name=f"{entity_type}_id", required=True)

    note = args.get("note")

    validate_note_add_command_args(entity_id=entity_id, note=note, entity_type=entity_type)  # type: ignore
    # Call Vectra API to add note
    notes = client.add_note_request(entity_id=entity_id, entity_type=entity_type, note=note)  # type: ignore

    notes_raw_response = deepcopy(notes)
    output_prefix = OUTPUT_PREFIXES[f"{entity_type.upper()}_NOTES"]
    if notes:
        notes["note_id"] = notes.get("id")
        del notes["id"]
        notes.update({f"{entity_type}_id": entity_id})

    human_readable = f"##### The note has been successfully added to the {entity_type}."
    human_readable += f"\nReturned Note ID: **{notes['note_id']}**"

    return CommandResults(
        outputs_prefix=output_prefix,
        outputs=createContext(remove_empty_elements(notes)),
        readable_output=human_readable,
        raw_response=notes_raw_response,
        outputs_key_field=NOTE_OUTPUT_KEY_FIELD,
    )


def note_update_command(client: Client, entity_type: str, args: dict) -> CommandResults:
    """
    Updates a note to an Account/Host/Detection in Vectra API.

    Args:
        client: An instance of the Client class.
        entity_type (str): The type of the object (account, host or detection).

    Returns:
        CommandResults: The command results containing the outputs, readable output, raw response, and outputs key field.
    """
    entity_id = arg_to_number(args.get(f"{entity_type}_id"), arg_name=f"{entity_type}_id", required=True)
    note_id = arg_to_number(args.get("note_id"), arg_name="note_id", required=True)
    note = args.get("note")

    validate_note_update_command_args(entity_id=entity_id, note_id=note_id, note=note, entity_type=entity_type)  # type: ignore

    # Call Vectra API to update note
    notes = client.update_note_request(
        entity_id=entity_id,
        entity_type=entity_type,  # type: ignore
        note=note,
        note_id=note_id,
    )  # type: ignore
    notes_raw_response = deepcopy(notes)
    output_prefix = OUTPUT_PREFIXES[f"{entity_type.upper()}_NOTES"]
    if notes:
        notes["note_id"] = notes["id"]
        del notes["id"]
        notes.update({f"{entity_type}_id": entity_id})

    human_readable = f"##### The note has been successfully updated in the {entity_type}."

    return CommandResults(
        outputs_prefix=output_prefix,
        outputs=createContext(remove_empty_elements(notes)),
        readable_output=human_readable,
        raw_response=notes_raw_response,
        outputs_key_field=NOTE_OUTPUT_KEY_FIELD,
    )


def note_remove_command(client: Client, entity_type: str, args: dict) -> CommandResults:
    """
    Removes a note from an Account/Host/Detection in Vectra API.

    Args:
        client (VectraClient): An instance of the VectraClient class.

    Returns:
        CommandResults: The command results containing the outputs, readable output.
    """
    entity_id = arg_to_number(args.get(f"{entity_type}_id"), arg_name=f"{entity_type}_id", required=True)
    note_id = arg_to_number(args.get("note_id"), arg_name="note_id", required=True)

    validate_note_remove_command_args(entity_id=entity_id, note_id=note_id, entity_type=entity_type)

    # Call Vectra API to remove note
    response = client.remove_note_request(entity_id=entity_id, entity_type=entity_type, note_id=note_id)  # type: ignore

    if response.status_code == 204:
        human_readable = f"##### The note has been successfully removed from the {entity_type}."
    else:
        human_readable = f"Something went wrong. API Response: {response.text}"

    return CommandResults(readable_output=human_readable, raw_response=response.text)


def note_list_command(client: Client, entity_type: str, args: dict) -> CommandResults:  # type: ignore
    """
    List notes of specific Account, Host or Detection.

    Args:
        client: An instance of the Client class.
        entity_type: The type Account, Host or Detection
    Returns:
        CommandResults: The command results containing the outputs, readable output, raw response, and outputs key field.
    """
    entity_id = arg_to_number(args.get(f"{entity_type}_id"), arg_name=f"{entity_type}_id", required=True)

    validate_positive_integer_arg(entity_id, arg_name=f"{entity_type}_id", required=True)

    notes = client.list_note_request(entity_id=entity_id, entity_type=entity_type)  # type: ignore
    notes_raw_response = deepcopy(notes)
    notes = remove_empty_elements(notes)
    if notes:
        human_readable = get_list_notes_command_hr(notes, entity_id, entity_type)  # type: ignore

        context = [createContext(note) for note in notes]

        output_prefix = OUTPUT_PREFIXES[f"{entity_type.upper()}_NOTES"]  # type: ignore

        return CommandResults(
            outputs_prefix=output_prefix,
            outputs=context,
            readable_output=human_readable,
            raw_response=notes_raw_response,
            outputs_key_field=NOTE_OUTPUT_KEY_FIELD,
        )
    return CommandResults(
        outputs={}, raw_response=notes_raw_response, readable_output=f"Couldn't find any notes for provided {entity_type}."
    )


def markall_detections_asfixed_command(
    client: Client,
    type: str,
    account_id: str = None,  # type: ignore
    host_id: str = None,
) -> CommandResults:  # type: ignore
    """
    Marks all active detections of an account/host as fixed.

    - param:
        - client: Vectra Client
        - type: The object to work with ("account" or "host")

    - return:
        CommandResults to be used in War Room.
    """
    if type == "account":
        validate_positive_integer_arg(account_id, "account_id", True)
        entity_data = client.get_account_by_account_id(account_id=str(account_id))
    else:
        validate_positive_integer_arg(host_id, "host_id", True)
        entity_data = client.get_host_by_host_id(host_id=str(host_id))

    detections_ids = [
        str(detection.get("detection_id"))
        for detection in entity_data.get("detection_summaries")
        if detection.get("state") == "active"
    ]

    if detections_ids:
        api_response = client.markasfixed_by_detection_ids(ids_list=detections_ids)

        # 404 API error will be raised by the Client class
        command_result = CommandResults(
            readable_output=f"The active detections of the provided {type} have been successfully marked as fixed.",
            raw_response=api_response,
        )
    else:
        command_result = CommandResults(readable_output="There are no active detections present.", raw_response={})

    return command_result


def markall_detections_asclosed_command(
    client: Client,
    entity_type: str,
    args: dict[str, Any],
) -> CommandResults:
    """
    Marks all active detections of an account/host as closed.

    - param:
        - client: Vectra Client
        - entity_type: The object to work with ("account" or "host")
        - args: The arguments

    - return:
        CommandResults to be used in War Room.
    """
    entity_id = args.get(f"{entity_type}_id")

    validate_positive_integer_arg(entity_id, arg_name=f"{entity_type}_id", required=True)

    close_reason = args.get("close_reason", "").lower()
    if not close_reason:
        raise ValueError(ERRORS["REQUIRED_ARGUMENT"].format("close_reason"))
    # Validate close_reason
    if close_reason not in VALID_CLOSE_REASON:
        raise ValueError('Invalid close_reason. Must be "benign" or "remediated".')

    # Get account data
    if entity_type == "account":
        entity_data = client.get_account_by_account_id(account_id=str(entity_id))
    else:
        entity_data = client.get_host_by_host_id(host_id=str(entity_id))

    # Extract active detection IDs
    detections_ids = [
        str(detection.get("detection_id"))
        for detection in entity_data.get("detection_summaries")
        if detection.get("state") == "active"
    ]

    if detections_ids:
        api_response = client.close_detections_by_ids(ids_list=detections_ids, reason=close_reason)
        if api_response.get("_meta", {}).get("level", "").lower() != "success":
            res_message = api_response.get("_meta", {}).get("message", "")
            message = "Something went wrong."
            if res_message:
                message += f" Message: {res_message}."
            raise DemistoException(message)

        # 404 API error will be raised by the Client class
        hr_output = f"##### The active detections of the provided {entity_type} have been successfully closed as {close_reason}."
        command_result = CommandResults(
            readable_output=hr_output,
            raw_response=api_response,
        )
    else:
        command_result = CommandResults(readable_output="##### There are no active detections present.", raw_response={})

    return command_result


def vectra_group_list_command(client: Client, args: dict[str, Any]):
    """
    Retrieves a list of groups.

    Args:
        client (Client): An instance of the Client class.
        args (Dict[str, Any]): The command arguments provided by the user.

    Returns:
        CommandResults: The command results containing the outputs, readable output, raw response, and outputs key field.
    """
    validate_group_list_command_args(args)

    # Get function arguments
    group_type = args.get("group_type", "")
    if group_type:
        group_type = group_type.lower()
    importance = args.get("importance", "")
    if importance:
        importance = importance.lower()
    account_names = argToList(args.get("account_names", ""))
    domains = argToList(args.get("domains", ""))
    host_ids = argToList(args.get("host_ids", ""))
    host_names = argToList(args.get("host_names", ""))
    ips = argToList(args.get("ips", ""))
    description = args.get("description", "")
    last_modified_timestamp = arg_to_datetime(args.get("last_modified_timestamp"), arg_name="last_modified_timestamp")
    last_modified_by = args.get("last_modified_by", "")
    group_name = args.get("group_name", "")

    response = client.list_group_request(
        group_type=group_type,
        account_names=account_names,
        domains=domains,
        host_ids=host_ids,
        host_names=host_names,
        importance=importance,
        ips=ips,
        description=description,
        last_modified_timestamp=last_modified_timestamp,
        last_modified_by=last_modified_by,
        group_name=group_name,
    )  # type: ignore
    count = response.get("count")
    if count == 0:
        return CommandResults(
            outputs={}, readable_output="##### Couldn't find any matching groups for provided filters.", raw_response=response
        )
    groups = response.get("results")

    # Prepare context data
    human_readable = get_group_list_command_hr(groups)  # type: ignore
    context = [createContext(group) for group in remove_empty_elements(groups)]  # type: ignore

    return CommandResults(
        outputs_prefix="Vectra.Group",
        outputs=context,
        readable_output=human_readable,
        raw_response=groups,
        outputs_key_field="group_id",
    )


def vectra_group_unassign_command(client: Client, args: dict[str, Any]):
    """
    Unassign members in Group.

    Args:
        client (Client): An instance of the Client class.
        args (Dict[str, Any]): The command arguments.

    Returns:
        CommandResults: The command results.
    """
    validate_group_assign_and_unassign_command_args(args)
    group_id = args.get("group_id")
    members = args.get("members")

    # Call to get group details
    group = client.get_group_request(group_id=group_id)  # type: ignore
    group_type = group.get("type")
    updated_members = group_members = group.get("members")
    members_list = argToList(members)
    removed_members = []

    if group_type.lower() == "ip" or group_type.lower() == "domain":  # type: ignore
        for member in members_list:
            if member in group_members:  # type: ignore
                removed_members.append(member)
                updated_members.remove(member)  # type: ignore
    elif group_type.lower() == "account":  # type: ignore
        uids = [i.get("uid") for i in group_members]  # type: ignore
        for member in members_list:
            if member in uids:
                removed_members.append(member)
                uids.remove(member)
        updated_members = uids
    elif group_type.lower() == "host":  # type: ignore
        ids = [str(i.get("id")) for i in group_members]  # type: ignore
        for member in members_list:
            if member in ids:
                removed_members.append(member)
                ids.remove(member)
        updated_members = ids
    if not removed_members:
        return CommandResults(readable_output=f"##### Member(s) {', '.join(members_list)} do not exist in the group.")
    # Call Vectra API to unassign members in group
    res = client.update_group_members_request(group_id=group_id, members=updated_members)  # type: ignore
    updated_group = remove_empty_elements(res)

    human_readable = get_group_unassign_and_assign_command_hr(
        group=updated_group, changed_members=removed_members, assign_flag=False
    )

    return CommandResults(
        outputs_prefix="Vectra.Group",
        outputs=createContext(updated_group),
        readable_output=human_readable,
        raw_response=updated_group,
        outputs_key_field="group_id",
    )


def vectra_group_assign_command(client: Client, args: dict[str, Any]):
    """
    Assign members in Group.

    Args:
        client (Client): An instance of the Client class.
        args (Dict[str, Any]): The command arguments.

    Returns:
        CommandResults: The command results.
    """
    validate_group_assign_and_unassign_command_args(args)
    group_id = args.get("group_id")
    members = args.get("members")

    # Call to get group details
    group = client.get_group_request(group_id=group_id)  # type: ignore
    group_type = group.get("type")
    updated_members = group_members = group.get("members")
    members_list = argToList(members)
    added_members = []

    if group_type.lower() == "ip" or group_type.lower() == "domain":  # type: ignore
        for member in members_list:
            if member not in group_members:  # type: ignore
                added_members.append(member)
                updated_members.append(member)  # type: ignore
    elif group_type.lower() == "account":  # type: ignore
        uids = [i.get("uid") for i in group_members]  # type: ignore
        for member in members_list:
            if member not in uids:
                added_members.append(member)
                uids.append(member)
        updated_members = uids
    elif group_type.lower() == "host":  # type: ignore
        ids = [str(i.get("id")) for i in group_members]  # type: ignore
        for member in members_list:
            if member not in ids:
                added_members.append(member)
                ids.append(member)
        updated_members = ids
    if not added_members:
        return CommandResults(readable_output=f"##### Member(s) {', '.join(members_list)} are already in the group.")
    # Call Vectra API to assign members in group
    res = client.update_group_members_request(group_id=group_id, members=updated_members)  # type: ignore
    updated_group = remove_empty_elements(res)

    human_readable = get_group_unassign_and_assign_command_hr(
        group=updated_group, changed_members=added_members, assign_flag=True
    )

    return CommandResults(
        outputs_prefix="Vectra.Group",
        outputs=createContext(updated_group),
        readable_output=human_readable,
        raw_response=updated_group,
        outputs_key_field="group_id",
    )


def mark_detections_asclosed_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    Mark the detection as closed by providing ID of detections and close reason in the argument.

    - params:
        - client: Vectra Client
        - args: Command arguments
    """

    detection_ids = argToList(args.get("detection_ids"))
    detection_ids = [detection_id.strip() for detection_id in detection_ids if detection_id.strip()]

    # Validate detection ids
    if not detection_ids:
        raise ValueError(ERRORS["REQUIRED_ARGUMENT"].format("detection_ids"))
    all(validate_positive_integer_arg(detection_id, arg_name="detection_ids") for detection_id in detection_ids)

    close_reason = args.get("close_reason", "").lower()
    if not close_reason:
        raise ValueError(ERRORS["REQUIRED_ARGUMENT"].format("close_reason"))

    # Validate close_reason
    if close_reason not in VALID_CLOSE_REASON:
        raise ValueError("Invalid close_reason. Must be 'benign' or 'remediated'.")

    api_response = client.close_detections_by_ids(ids_list=detection_ids, reason=close_reason)
    if api_response.get("_meta", {}).get("level", "").lower() == "success":
        readable_output = f"##### The provided detection IDs have been successfully closed as {close_reason}."
    else:
        res_message = api_response.get("_meta", {}).get("message", "")
        message = "Something went wrong."
        if res_message:
            message += f" Message: {res_message}."
        raise DemistoException(message)

    command_result = CommandResults(readable_output=readable_output, raw_response=api_response)

    return command_result


def mark_detections_asopen_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    Open detection with provided detection IDs.

    Args:
        client (Client): An instance of the Client class.
        args (Dict[str, Any]): The command arguments.

    Raises:
        ValueError: If detection_ids argument is missing or empty.

    Returns:
        CommandResults: The command results.
    """
    # Get function arguments
    detection_ids = argToList(args.get("detection_ids"))
    detection_ids = [detection_id.strip() for detection_id in detection_ids if detection_id.strip()]

    # Validate detection_ids
    if not detection_ids:
        raise ValueError(ERRORS["REQUIRED_ARGUMENT"].format("detection_ids"))
    all(validate_positive_integer_arg(detection_id, arg_name="detection_ids") for detection_id in detection_ids)

    # Call Vectra API to open detections
    api_response = client.open_detections_by_ids(ids_list=detection_ids)

    if api_response.get("_meta", {}).get("level", "").lower() == "success":
        readable_output = "##### The provided detection IDs have been successfully re-opened."
    else:
        res_message = api_response.get("_meta", {}).get("message", "")
        message = "Something went wrong."
        if res_message:
            message += f" Message: {res_message}."
        raise DemistoException(message)

    command_result = CommandResults(readable_output=readable_output, raw_response=api_response)

    return command_result


# ####           #### #
# ## MAIN FUNCTION ## #


def main() -> None:  # pragma: no cover
    # Set some settings as global (to use them inside some functions)
    global global_UI_URL

    integration_params = remove_space_from_args(demisto.params())
    remove_nulls_from_dictionary(integration_params)
    command = demisto.command()
    kwargs = remove_space_from_args(demisto.args())
    remove_nulls_from_dictionary(kwargs)

    server_fqdn: str | None = integration_params.get("server_fqdn")
    if not server_fqdn:  # Should be impossible thx to UI required settings control
        raise DemistoException("Missing integration setting : 'Server FQDN'")

    auth_type = integration_params.get("authentication_type", "API Token").lower()
    use_oauth = auth_type == "oauth 2.0"

    credentials: dict | None = integration_params.get("credentials")
    oauth_credentials: dict | None = integration_params.get("oauth_credentials")

    # Handle different authentication types
    api_token: str | None = None
    client_id: str | None = None
    client_secret: str | None = None

    if use_oauth:
        # OAuth 2.0 authentication
        if not oauth_credentials:
            raise DemistoException(
                "Missing integration setting : 'OAuth 2.0 Credentials' are required for OAuth 2.0 authentication"
            )

        client_id = oauth_credentials.get("identifier", "").strip()
        client_secret = oauth_credentials.get("password", "").strip()

        if not client_id or not client_secret:
            raise DemistoException(
                "Missing integration setting : 'Client ID' and 'Client Secret' are required for OAuth 2.0 authentication"
            )
    else:
        # API Token authentication (default)
        if not credentials:
            raise DemistoException("Missing integration setting : 'Credentials' or 'API token'")

        api_token = credentials.get("password", "").strip()
        if (api_token is None) or (api_token == ""):
            raise DemistoException("Missing integration setting : 'Credentials password' or 'API token'")

    # Setting default settings for fetch mode
    if integration_params.get("isFetch"):
        if integration_params.get("first_fetch") == "":
            integration_params["first_fetch"] = DEFAULT_FIRST_FETCH
            demisto.debug(f"First fetch timestamp not set, setting to default '{DEFAULT_FIRST_FETCH}'")
        if integration_params.get("fetch_entity_types") == []:
            integration_params["fetch_entity_types"] = DEFAULT_FETCH_ENTITY_TYPES
            demisto.debug(f"Fetch entity types not set, setting to default '{DEFAULT_FETCH_ENTITY_TYPES}'")
        if integration_params.get("max_fetch") == "":
            integration_params["max_fetch"] = DEFAULT_MAX_FETCH
            demisto.debug(f"Max incidents per fetch not set, setting to default '{DEFAULT_MAX_FETCH}'")

    verify_certificate: bool = not integration_params.get("insecure", False)
    use_proxy: bool = integration_params.get("use_proxy", False)

    global_UI_URL = urljoin("https://", server_fqdn)
    api_base_url = urljoin("https://", urljoin(server_fqdn, API_VERSION_URL))

    demisto.info(f"Command being called is {command}")
    try:
        # Create client with appropriate authentication
        client = Client(
            base_url=api_base_url,
            verify=verify_certificate,
            proxy=use_proxy,
            use_oauth=use_oauth,
            api_token=api_token,
            client_id=client_id,
            client_secret=client_secret,
        )

        if command == "test-module":
            # This is the call made when pressing the integration Test button.
            results = test_module(client, integration_params)
            return_results(results)

        elif command == "fetch-incidents":
            # Get new incidents to create if any from Vectra API
            next_run, incidents = fetch_incidents(client, integration_params)

            # Add incidents in the SOAR platform
            demisto.incidents(incidents)

            if next_run:
                demisto.info(f"Setting last run to : {next_run}")
                demisto.setLastRun(next_run)
            demisto.info("fetch-incidents action done.")

        elif command == "get-modified-remote-data":
            return_results(get_modified_remote_data_command(client))  # type: ignore
        elif command == "get-remote-data":
            return_results(get_remote_data_command(client, integration_params))  # type: ignore
        elif command == "update-remote-system":
            return_results(update_remote_system_command(client))

        elif command == "vectra-search-accounts":
            return_results(vectra_search_accounts_command(client, **kwargs))
        elif command == "vectra-search-hosts":
            return_results(vectra_search_hosts_command(client, **kwargs))
        elif command == "vectra-search-detections":
            return_results(vectra_search_detections_command(client, **kwargs))

        elif command == "vectra-search-assignments":
            return_results(vectra_search_assignments_command(client, **kwargs))
        elif command == "vectra-search-outcomes":
            return_results(vectra_search_outcomes_command(client, **kwargs))
        elif command == "vectra-search-users":
            return_results(vectra_search_users_command(client, **kwargs))

        # ## Accounts centric commands
        elif command == "vectra-account-describe":
            return_results(vectra_get_account_by_id_command(client, **kwargs))
        elif command == "vectra-account-add-tags":
            return_results(add_tags_command(client, type="account", **kwargs))
        elif command == "vectra-account-del-tags":
            return_results(del_tags_command(client, type="account", **kwargs))
        elif command == "vectra-account-tag-list":
            return_results(tag_list_command(client, entity_type="account", args=kwargs))
        elif command == "vectra-account-note-add":
            return_results(note_add_command(client, entity_type="account", args=kwargs))
        elif command == "vectra-account-note-update":
            return_results(note_update_command(client, entity_type="account", args=kwargs))
        elif command == "vectra-account-note-remove":
            return_results(note_remove_command(client, entity_type="account", args=kwargs))
        elif command == "vectra-account-note-list":
            return_results(note_list_command(client, entity_type="account", args=kwargs))
        elif command == "vectra-account-markall-detections-asfixed":
            return_results(markall_detections_asfixed_command(client, type="account", **kwargs))
        elif command == "vectra-account-markall-detections-asclosed":
            return_results(markall_detections_asclosed_command(client, entity_type="account", args=kwargs))

        # ## Hosts centric commands
        elif command == "vectra-host-describe":
            return_results(vectra_get_host_by_id_command(client, **kwargs))
        elif command == "vectra-host-add-tags":
            return_results(add_tags_command(client, type="host", **kwargs))
        elif command == "vectra-host-del-tags":
            return_results(del_tags_command(client, type="host", **kwargs))
        elif command == "vectra-host-tag-list":
            return_results(tag_list_command(client, entity_type="host", args=kwargs))
        elif command == "vectra-host-note-add":
            return_results(note_add_command(client, entity_type="host", args=kwargs))
        elif command == "vectra-host-note-update":
            return_results(note_update_command(client, entity_type="host", args=kwargs))
        elif command == "vectra-host-note-remove":
            return_results(note_remove_command(client, entity_type="host", args=kwargs))
        elif command == "vectra-host-note-list":
            return_results(note_list_command(client, entity_type="host", args=kwargs))
        elif command == "vectra-host-markall-detections-asfixed":
            return_results(markall_detections_asfixed_command(client, type="host", **kwargs))
        elif command == "vectra-host-markall-detections-asclosed":
            return_results(markall_detections_asclosed_command(client, entity_type="host", args=kwargs))

        # ## Detections centric commands
        elif command == "vectra-detection-describe":
            return_results(vectra_get_detection_by_id_command(client, **kwargs))
        elif command == "vectra-detection-get-pcap":
            return_results(get_detection_pcap_file_command(client, **kwargs))
        elif command == "vectra-detection-markasfixed":
            return_results(mark_detection_as_fixed_command(client, **kwargs))
        elif command == "vectra-detection-add-tags":
            return_results(add_tags_command(client, type="detection", **kwargs))
        elif command == "vectra-detection-del-tags":
            return_results(del_tags_command(client, type="detection", **kwargs))
        elif command == "vectra-detection-tag-list":
            return_results(tag_list_command(client, entity_type="detection", args=kwargs))
        elif command == "vectra-detection-note-add":
            return_results(note_add_command(client, entity_type="detection", args=kwargs))
        elif command == "vectra-detection-note-update":
            return_results(note_update_command(client, entity_type="detection", args=kwargs))
        elif command == "vectra-detection-note-remove":
            return_results(note_remove_command(client, entity_type="detection", args=kwargs))
        elif command == "vectra-detection-note-list":
            return_results(note_list_command(client, entity_type="detection", args=kwargs))
        elif command == "vectra-detections-mark-asclosed":
            return_results(mark_detections_asclosed_command(client, args=kwargs))
        elif command == "vectra-detections-mark-asopen":
            return_results(mark_detections_asopen_command(client, args=kwargs))

        # ## Assignments / Assignment outcomes commands
        elif command == "vectra-assignment-describe":
            return_results(vectra_get_assignment_by_id_command(client, **kwargs))
        elif command == "vectra-assignment-assign":
            return_results(vectra_assignment_assign_command(client, **kwargs))
        elif command == "vectra-assignment-resolve":
            return_results(vectra_assignment_resolve_command(client, **kwargs))
        elif command == "vectra-outcome-describe":
            return_results(vectra_get_outcome_by_id_command(client, **kwargs))
        elif command == "vectra-outcome-create":
            return_results(vectra_outcome_create_command(client, **kwargs))
        elif command == "vectra-user-describe":
            return_results(vectra_get_user_by_id_command(client, **kwargs))

        # ## Groups / Groups outcomes commands
        elif command == "vectra-group-list":
            return_results(vectra_group_list_command(client, kwargs))
        elif command == "vectra-group-assign":
            return_results(vectra_group_assign_command(client, kwargs))
        elif command == "vectra-group-unassign":
            return_results(vectra_group_unassign_command(client, kwargs))

        else:
            raise NotImplementedError

    # Log exceptions and return errors
    except Exception as e:
        return_error(f"Failed to execute {command} command.\nError:\n{e!s}")


# ####         #### #
# ## ENTRY POINT ## #
if __name__ in ("__main__", "__builtin__", "builtins"):  # pragma: no cover
    main()