PATHelpdeskAdvanced

Improve the effectiveness of your service provision and resources, and the quality of your IT department.

Utilities · PAT Helpdesk Advanced

Details

IDPATHelpdeskAdvanced
ProviderPATServicesLTD
CategoryUtilities
From Version6.9.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

Improve the effectiveness of your service provision and resources, and the quality of your IT department.
This integration was integrated and tested with version 11.2.3 of PATHelpdeskAdvanced.

Configure PAT HelpdeskAdvanced in Cortex

Parameter Required
PAT Helpdesk URL True
Username True
Password True
Trust any certificate (not secure) False
Use system proxy settings False

Commands

You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.

hda-create-ticket


Create a new ticket.

Base Command

hda-create-ticket

Input

Argument Name Description Required
object_type_id The object type ID for the ticket. Required
ticket_status_id The status ID for the ticket. Use hda-list-ticket-statuses to get a list of status IDs. Required
ticket_priority_id The priority ID for the ticket. Use hda-list-ticket-priorities to get a list of priority IDs. Required
object_description The description of the object. Optional
ticket_classification_id The classification ID for the ticket. Optional
ticket_type_id The type ID for the ticket. Optional
contact_id The contact ID for the ticket. Optional
subject The subject for the ticket. Optional
problem The problem description for the ticket. Optional
site The site for the ticket. Optional

Context Output

Path Type Description
HelpdeskAdvanced.Ticket.AccountID string The account ID of the ticket.
HelpdeskAdvanced.Ticket.AssetCategoryID string The asset category ID of the ticket.
HelpdeskAdvanced.Ticket.AssetID string The asset ID of the ticket.
HelpdeskAdvanced.Ticket.AssignedUserGroupID string The assigned user group ID of the ticket.
HelpdeskAdvanced.Ticket.AssignedUserID string The assigned user ID of the ticket.
HelpdeskAdvanced.Ticket.AssignedUserOrGroupID string The assigned user or group ID of the ticket.
HelpdeskAdvanced.Ticket.BilledTokens string The billed tokens of the ticket.
HelpdeskAdvanced.Ticket.BusinessFunctionID string The business function ID of the ticket.
HelpdeskAdvanced.Ticket.C12 string The C12 field of the ticket.
HelpdeskAdvanced.Ticket.C13 string The C13 field of the ticket.
HelpdeskAdvanced.Ticket.C134C string The C134C field of the ticket.
HelpdeskAdvanced.Ticket.C14 string The C14 field of the ticket.
HelpdeskAdvanced.Ticket.C15 string The C15 field of the ticket.
HelpdeskAdvanced.Ticket.CalendarID string The calendar ID of the ticket.
HelpdeskAdvanced.Ticket.ClosedByUserID string The ID of the user who closed the ticket.
HelpdeskAdvanced.Ticket.ClosureDate string The closure date of the ticket.
HelpdeskAdvanced.Ticket.ContactID string The contact ID of the ticket.
HelpdeskAdvanced.Ticket.CostCenterID string The cost center ID of the ticket.
HelpdeskAdvanced.Ticket.CustomerContractID string The customer contract ID of the ticket.
HelpdeskAdvanced.Ticket.Date date The date of the ticket.
HelpdeskAdvanced.Ticket.DefaultSolutionID string The default solution ID of the ticket.
HelpdeskAdvanced.Ticket.EstimatedTaskDuration string The estimated task duration of the ticket.
HelpdeskAdvanced.Ticket.EstimatedTaskStartDate string The estimated task start date of the ticket.
HelpdeskAdvanced.Ticket.ExpirationDate date The expiration date of the ticket.
HelpdeskAdvanced.Ticket.FirstUpdateUserID string The ID of the user who first updated the ticket.
HelpdeskAdvanced.Ticket.FullText string The full text of the ticket.
HelpdeskAdvanced.Ticket.ID string The ID of the ticket.
HelpdeskAdvanced.Ticket.ImpactID string The impact ID of the ticket.
HelpdeskAdvanced.Ticket.IsNew boolean Whether the ticket is new or not.
HelpdeskAdvanced.Ticket.KnownIssue string The known issue field of the ticket.
HelpdeskAdvanced.Ticket.LanguageID string The language ID of the ticket.
HelpdeskAdvanced.Ticket.LastExpirationDate date The last expiration date of the ticket.
HelpdeskAdvanced.Ticket.LastExpirationID string The last expiration ID of the ticket.
HelpdeskAdvanced.Ticket.LocationID string The location ID of the ticket.
HelpdeskAdvanced.Ticket.MailBoxID string The mailbox ID of the ticket.
HelpdeskAdvanced.Ticket.NextExpirationDate string The next expiration date of the ticket.
HelpdeskAdvanced.Ticket.NextExpirationID string The next expiration ID of the ticket.
HelpdeskAdvanced.Ticket.ObjectDescription string The object description of the ticket.
HelpdeskAdvanced.Ticket.ObjectEntity string The object entity of the ticket.
HelpdeskAdvanced.Ticket.ObjectTypeID string The object type ID of the ticket.
HelpdeskAdvanced.Ticket.OwnerUserGroupID string The owner user group ID of the ticket.
HelpdeskAdvanced.Ticket.OwnerUserID string The owner user ID of the ticket.
HelpdeskAdvanced.Ticket.ParentRecurringTicketID string The parent recurring ticket ID of the ticket.
HelpdeskAdvanced.Ticket.ParentTicketID string The parent ticket ID of the ticket.
HelpdeskAdvanced.Ticket.Problem string The problem field of the ticket.
HelpdeskAdvanced.Ticket.ProblemHTML string The problem field in HTML format of the ticket.
HelpdeskAdvanced.Ticket.RemoteID string The remote ID of the ticket.
HelpdeskAdvanced.Ticket.Score string The score of the ticket.
HelpdeskAdvanced.Ticket.ServiceID string The service ID of the ticket.
HelpdeskAdvanced.Ticket.Site string The site of the ticket.
HelpdeskAdvanced.Ticket.SiteUnRead string Whether the ticket is unread for the site.
HelpdeskAdvanced.Ticket.Solicits string Whether the ticket solicits feedback.
HelpdeskAdvanced.Ticket.SolutionHTML string The solution in HTML format of the ticket.
HelpdeskAdvanced.Ticket.SourceMailBoxID string The source mailbox ID of the ticket.
HelpdeskAdvanced.Ticket.Subject string The subject of the ticket.
HelpdeskAdvanced.Ticket.SupplierID string The supplier ID of the ticket.
HelpdeskAdvanced.Ticket.Tag string The tag of the ticket.
HelpdeskAdvanced.Ticket.TaskEffort string The task effort of the ticket.
HelpdeskAdvanced.Ticket.TemplateTicketID string The template ticket ID of the ticket.
HelpdeskAdvanced.Ticket.TicketClassificationID string The ticket classification ID of the ticket.
HelpdeskAdvanced.Ticket.TicketCode string The ticket code of the ticket.
HelpdeskAdvanced.Ticket.TicketPriorityID string The ticket priority ID of the ticket.
HelpdeskAdvanced.Ticket.TicketSolutionID string The ticket solution ID of the ticket.
HelpdeskAdvanced.Ticket.TicketSourceID string The ticket source ID of the ticket.
HelpdeskAdvanced.Ticket.TicketStatusID string The ticket status ID of the ticket.
HelpdeskAdvanced.Ticket.TicketTypeID string The ticket type ID of the ticket.
HelpdeskAdvanced.Ticket.UnRead string Whether the ticket is unread.
HelpdeskAdvanced.Ticket.UrgencyID string The urgency ID of the ticket.
HelpdeskAdvanced.Ticket.UserID string The user ID of the ticket.

Command example

!hda-create-ticket object_type_id=T2 ticket_priority_id=P4 ticket_status_id=S3

Context Example

{
    "HelpdeskAdvanced": {
        "Ticket": {
            "data": {
                "AccountID": "",
                "AssetCategoryID": "",
                "AssetID": "",
                "AssignedUserGroupID": "",
                "AssignedUserID": "S00000C",
                "AssignedUserOrGroupID": "S00000C",
                "BilledTokens": 0,
                "BusinessFunctionID": "",
                "C12": null,
                "C13": null,
                "C134C": null,
                "C14": null,
                "C15": null,
                "CalendarID": "",
                "ClosedByUserID": "",
                "ClosureDate": null,
                "ContactID": "",
                "CostCenterID": "",
                "CustomerContractID": "",
                "Date": "2023-10-04T07:45:09Z",
                "DefaultSolutionID": "",
                "EstimatedTaskDuration": 0,
                "EstimatedTaskStartDate": null,
                "ExpirationDate": "2023-10-04T15:45:00Z",
                "FirstUpdateUserID": "S00000C",
                "FullText": "",
                "ID": "12345678C",
                "ImpactID": "",
                "IsNew": false,
                "KnownIssue": false,
                "LanguageID": "0",
                "LastExpirationDate": "2998-12-31T23:00:00Z",
                "LastExpirationID": "",
                "LocationID": "",
                "MailBoxID": "DEF_MBOX",
                "NextExpirationDate": "2998-12-31T23:00:00Z",
                "NextExpirationID": "",
                "ObjectDescription": "12345678C: ",
                "ObjectEntity": "Incident",
                "ObjectTypeID": "T2",
                "OwnerUserGroupID": "G66C",
                "OwnerUserID": "S00000C",
                "ParentRecurringTicketID": "",
                "ParentTicketID": "",
                "Problem": "",
                "ProblemHTML": "",
                "RemoteID": "",
                "Score": 0,
                "ServiceID": "",
                "Site": null,
                "SiteUnRead": true,
                "Solicits": 0,
                "SolutionHTML": "",
                "SourceMailBoxID": "DEFAULT",
                "Subject": "",
                "SupplierID": "",
                "Tag": "",
                "TaskEffort": 0,
                "TemplateTicketID": "",
                "TicketClassificationID": "INC",
                "TicketCode": "",
                "TicketPriorityID": "P4",
                "TicketSolutionID": "",
                "TicketSourceID": "",
                "TicketStatusID": "S3",
                "TicketTypeID": "T2",
                "UnRead": true,
                "UrgencyID": "",
                "UserID": ""
            },
            "requestToken": "token",
            "result": {
                "code": "0",
                "desc": "",
                "parameters": null,
                "subcode": null
            },
            "success": true,
            "total": 1
        }
    }
}

Human Readable Output

Ticket Created

Object Description Object Entity Ticket Classification ID Is New Expiration Date First Update User ID Owner User ID Date Assigned User ID
12345678C: Incident INC false 2023-10-04T15:45:00Z S00000C S00000C 2023-10-04T07:45:09Z S00000C

hda-list-tickets


List tickets.

Base Command

hda-list-tickets

Input

Argument Name Description Required
ticket_id The ID of the ticket to retrieve. Optional
filter Filters to apply to the ticket list. Optional
page The page number to retrieve. Optional
page_size The number of tickets per page. Optional
limit The maximum number of tickets to return. Default is 50. Optional

Context Output

Path Type Description
HelpdeskAdvanced.Ticket.AccountID string The account ID of the ticket.
HelpdeskAdvanced.Ticket.AssetID string The asset ID of the ticket.
HelpdeskAdvanced.Ticket.BilledTokens string The billed tokens of the ticket.
HelpdeskAdvanced.Ticket.CalendarID string The calendar ID of the ticket.
HelpdeskAdvanced.Ticket.ClosureDate string The closure date of the ticket.
HelpdeskAdvanced.Ticket.ContactID string The contact ID of the ticket.
HelpdeskAdvanced.Ticket.CustomerContractID string The customer contract ID of the ticket.
HelpdeskAdvanced.Ticket.Date string The date of the ticket.
HelpdeskAdvanced.Ticket.EstimatedTaskDuration string The estimated task duration of the ticket.
HelpdeskAdvanced.Ticket.EstimatedTaskStartDate string The estimated task start date of the ticket.
HelpdeskAdvanced.Ticket.ExpirationDate string The expiration date of the ticket.
HelpdeskAdvanced.Ticket.FirstUpdateUserID string The ID of the user who first updated the ticket.
HelpdeskAdvanced.Ticket.ID string The ID of the ticket.
HelpdeskAdvanced.Ticket.KnownIssue string The known issue field of the ticket.
HelpdeskAdvanced.Ticket.LanguageID string The language ID of the ticket.
HelpdeskAdvanced.Ticket.LastExpirationDate string The last expiration date of the ticket.
HelpdeskAdvanced.Ticket.LocationID string The location ID of the ticket.
HelpdeskAdvanced.Ticket.MailBoxID string The mailbox ID of the ticket.
HelpdeskAdvanced.Ticket.NextExpirationDate string The next expiration date of the ticket.
HelpdeskAdvanced.Ticket.NextExpirationID string The next expiration ID of the ticket.
HelpdeskAdvanced.Ticket.OwnerUserID string The owner user ID of the ticket.
HelpdeskAdvanced.Ticket.Problem string The problem field of the ticket.
HelpdeskAdvanced.Ticket.ProblemHTML string The problem field in HTML format of the ticket.
HelpdeskAdvanced.Ticket.Score string The score of the ticket.
HelpdeskAdvanced.Ticket.ServiceID string The service ID of the ticket.
HelpdeskAdvanced.Ticket.SiteUnRead string Whether the ticket is unread for the site.
HelpdeskAdvanced.Ticket.Solicits string Whether the ticket solicits feedback.
HelpdeskAdvanced.Ticket.Solution string The solution of the ticket.
HelpdeskAdvanced.Ticket.SolutionHTML string The solution in HTML format of the ticket.
HelpdeskAdvanced.Ticket.Subject string The subject of the ticket.
HelpdeskAdvanced.Ticket.SupplierID string The supplier ID of the ticket.
HelpdeskAdvanced.Ticket.TaskEffort string The task effort of the ticket.
HelpdeskAdvanced.Ticket.TicketClassificationID string The ticket classification ID of the ticket.
HelpdeskAdvanced.Ticket.TicketTypeID string The ticket type ID of the ticket.
HelpdeskAdvanced.Ticket.UrgencyID string The urgency ID of the ticket.

Command example


#### Context Example

```json
{
    "HelpdeskAdvanced": {
        "Ticket": [
            {
                "AccountID": "C00C",
                "AssetID": null,
                "BilledTokens": 0,
                "CalendarID": null,
                "ClosureDate": "2019-04-28T07:04:19Z",
                "ContactID": "S24007C",
                "CustomerContractID": null,
                "Date": "2019-04-25T07:03:48Z",
                "EstimatedTaskDuration": 0,
                "EstimatedTaskStartDate": null,
                "ExpirationDate": "2019-04-28T15:03:00Z",
                "FirstUpdateUserID": "S00000C",
                "ID": "10000000C",
                "KnownIssue": false,
                "LanguageID": 5,
                "LastExpirationDate": "2019-04-29T10:00:00Z",
                "LocationID": "F3C",
                "MailBoxID": "DEF_MBOX",
                "NextExpirationDate": "2019-04-29T10:00:00Z",
                "NextExpirationID": "244C",
                "OwnerUserID": "S00000C",
                "Problem": "Problem",
                "ProblemHTML": "<p>Problem</p>",
                "Score": 0,
                "ServiceID": "S000X",
                "SiteUnRead": null,
                "Solicits": null,
                "Solution": null,
                "SolutionHTML": null,
                "Subject": "Problem",
                "SupplierID": null,
                "TaskEffort": null,
                "TicketClassificationID": "INC",
                "TicketTypeID": "T4",
                "UrgencyID": "U2"
            }
        ]
    }
}

Human Readable Output

Tickets

Ticket ID Subject Solution Date Service ID Problem Contact ID Owner User ID Account ID
10000000C Problem   2019-04-25T07:03:48Z S000X Problem S24007C S00000C C00C

hda-add-ticket-attachment


Add an attachment to a ticket.

Base Command

hda-add-ticket-attachment

Input

Argument Name Description Required
ticket_id The ticket ID to retrieve details for. Required
entry_id The file entry ID to attach. Required

Context Output

There is no context output for this command.

hda-list-ticket-attachments


List attachments for a ticket.

Base Command

hda-list-ticket-attachments

Input

Argument Name Description Required
ticket_id The ticket ID to retrieve details for. Required
limit The maximum number of tickets to return. Default is 50. Optional

Context Output

Path Type Description
HelpdeskAdvanced.Ticket.Attachment.BlobID string The blob ID of the attachment.
HelpdeskAdvanced.Ticket.Attachment.ContentType string The content type of the attachment.
HelpdeskAdvanced.Ticket.Attachment.Description string The description of the attachment.
HelpdeskAdvanced.Ticket.Attachment.EmailID string The email ID of the attachment.
HelpdeskAdvanced.Ticket.Attachment.FileName string The file name of the attachment.
HelpdeskAdvanced.Ticket.Attachment.FirstUpdate string The date and time when the attachment was first updated.
HelpdeskAdvanced.Ticket.Attachment.FirstUpdateUserID string The ID of the user who first uploaded the attachment.
HelpdeskAdvanced.Ticket.Attachment.ID string The ID of the attachment.
HelpdeskAdvanced.Ticket.Attachment.IsNew string Indicates whether the attachment is new.
HelpdeskAdvanced.Ticket.Attachment.KBSize string The size of the attachment in KB.
HelpdeskAdvanced.Ticket.Attachment.LastUpdate string The date and time when the attachment was last updated.
HelpdeskAdvanced.Ticket.Attachment.LastUpdateUserID string The ID of the user who last updated the attachment.
HelpdeskAdvanced.Ticket.Attachment.Note string Any additional notes about the attachment.
HelpdeskAdvanced.Ticket.Attachment.ObjectDescription string The description of the object the attachment is associated with.
HelpdeskAdvanced.Ticket.Attachment.ObjectEntity string The object entity of the attachment.
HelpdeskAdvanced.Ticket.Attachment.ObjectTypeID string The object type ID of the attachment.
HelpdeskAdvanced.Ticket.Attachment.OwnerUserID string The owner user ID of the attachment.
HelpdeskAdvanced.Ticket.Attachment.ParentObject string The parent object the attachment is associated with.
HelpdeskAdvanced.Ticket.Attachment.ParentObjectID string The ID of the parent object the attachment is associated with.
HelpdeskAdvanced.Ticket.Attachment.RemoteID string The remote ID of the attachment.
HelpdeskAdvanced.Ticket.Attachment.Site string The site ID the attachment is associated with.
HelpdeskAdvanced.Ticket.Attachment.TicketID string The ticket ID the attachment is associated with.
HelpdeskAdvanced.Ticket.Attachment.UniqueID string A unique ID for the attachment.

Command example

!hda-list-ticket-attachments ticket_id=A0000001C

Context Example

{
    "HelpdeskAdvanced": {
        "Ticket": {
            "Attachment": [
                {
                    "BlobID": "A0000000C",
                    "ContentType": "text/plain",
                    "Description": "test.txt",
                    "EmailID": "",
                    "FileName": "test.txt",
                    "FirstUpdate": null,
                    "FirstUpdateUserID": "S00000C",
                    "ID": "A0000000C",
                    "IsNew": false,
                    "KBSize": 0,
                    "LastUpdate": "2023-09-29T08:30:04Z",
                    "LastUpdateUserID": "",
                    "Note": "",
                    "ObjectDescription": "test.txt",
                    "ObjectEntity": "Attachment",
                    "ObjectTypeID": "DEFAULT",
                    "OwnerUserID": "",
                    "ParentObject": "Incident",
                    "ParentObjectID": "A0000001C",
                    "RemoteID": "",
                    "Site": null,
                    "TicketID": "A0000001C",
                    "UniqueID": "00000000-0000-0000-0000-000000000000"
                }
            ]
        }
    }
}

Human Readable Output

Attachments of A0000001C

File Name Last Update Description Object Description First Update User ID Object Entity Content Type
test.txt 2023-09-29T08:30:04Z test.txt test.txt S00000C Attachment text/plain

hda-add-ticket-comment


Add a comment to a ticket.

Base Command

hda-add-ticket-comment

Input

Argument Name Description Required
ticket_id The ticket ID to retrieve details for. Required
comment The comment text. Required
site_visible Whether the ticket is visible to the customer site. Possible values are: True, False. Default is False. Required

Context Output

There is no context output for this command.

Command example

!hda-add-ticket-comment comment="here be comment" site_visible=True ticket_id=A0000001C

Human Readable Output

Comment was succesfully added to A0000001C

hda-list-ticket-statuses


List ticket statuses.

Base Command

hda-list-ticket-statuses

Input

Argument Name Description Required
limit The maximum number of statuses to return. Default is 50. Optional

Context Output

Path Type Description
HelpdeskAdvanced.TicketStatus.ID Number The status ID.
HelpdeskAdvanced.TicketStatus.Name String The status name.

Command example


#### Context Example

```json
{
    "HelpdeskAdvanced": {
        "TicketStatus": [
            {
                "Description": "New",
                "ID": "S1"
            },
            {
                "Description": "Other",
                "ID": "S10"
            }
        ]
    }
}

Human Readable Output

Results

Description ID
New S1
Other S10

hda-change-ticket-status


Change the status of a ticket.

Base Command

hda-change-ticket-status

Input

Argument Name Description Required
ticket_id The ID of the ticket to change status for. Required
status_id The ID of the status to change the ticket to. Required
note An optional note to add with the status change. Optional

Context Output

There is no context output for this command.

Command example

!hda-change-ticket-status status_id=S2 ticket_id=1111111111C note=testing debug-mode=true

Human Readable Output

Changed status of ticket 1111111111C to S2 successfully.

hda-list-ticket-priorities


List ticket priorities.

Base Command

hda-list-ticket-priorities

Input

Argument Name Description Required
ticket_id The ID of the ticket to change status for. Required

Context Output

Path Type Description
HelpdeskAdvanced.TicketPriority.ID Number The priority ID.
HelpdeskAdvanced.TicketPriority.Name String The priority name.

hda-get-ticket-history


Get ticket history.

Base Command

hda-get-ticket-history

Input

Argument Name Description Required
ticket_id The ID of the ticket to change status for. Required

Context Output

Path Type Description
HelpdeskAdvanced.TicketHistory.TicketID string The ticket ID associated with the history entry.
HelpdeskAdvanced.TicketHistory.AccountID string The account ID of the ticket.
HelpdeskAdvanced.TicketHistory.Attachments string List of attachments for the ticket.
HelpdeskAdvanced.TicketHistory.AutEmailCounter string The AUT email counter.
HelpdeskAdvanced.TicketHistory.ContactID string The contact ID of the ticket.
HelpdeskAdvanced.TicketHistory.Data.Comment string The comment text.
HelpdeskAdvanced.TicketHistory.Data.Date string The date of the history entry.
HelpdeskAdvanced.TicketHistory.Data.From string The user who made the change.
HelpdeskAdvanced.TicketHistory.Data.FromID string The ID of the user who made the change.
HelpdeskAdvanced.TicketHistory.Data.Problem string The problem associated with the history entry.
HelpdeskAdvanced.TicketHistory.Data.To string The user/team the ticket was assigned to.
HelpdeskAdvanced.TicketHistory.Data.ToID string The ID of the user/team the ticket was assigned to.
HelpdeskAdvanced.TicketHistory.ExternalAction string The external action associated with the history entry.
HelpdeskAdvanced.TicketHistory.FullName string The full name of the ticket.
HelpdeskAdvanced.TicketHistory.HistoryID string The ID of the history entry.
HelpdeskAdvanced.TicketHistory.OperationDescription string The description of the operation.
HelpdeskAdvanced.TicketHistory.OperationTypeID string The type ID of the operation.
HelpdeskAdvanced.TicketHistory.UpdateDate date The date the ticket was last updated.
HelpdeskAdvanced.TicketHistory.UserID string The user ID of the ticket.
HelpdeskAdvanced.TicketHistory.Username string The username associated with the ticket.

Command example

!hda-get-ticket-history ticket_id=1111111C

Context Example

{
    "HelpdeskAdvanced": {
        "TicketHistory": {
            "AccountID": "",
            "Attachments": null,
            "TicketID": "1111111C",
            "AutEmailCounter": 0,
            "ContactID": "",
            "Data": {
                "Comment": "testing",
                "From": "Solved",
                "FromID": "S6",
                "To": "In Progress",
                "ToID": "S2"
            },
            "ExternalAction": false,
            "FullName": "John Doe",
            "HistoryID": 5667526,
            "OperationDescription": "Status change",
            "OperationTypeID": 20,
            "UpdateDate": "2023-10-04T07:45:35Z",
            "UserID": "S00000C",
            "Username": "username"
        }
    }
}

Human Readable Output

Ticket History: 1111111C

Update Date Ticket ID Operation Type ID Operation Description Full Name
2023-10-04T07:45:35Z 1111111C   Status change John Doe

hda-list-users


List users.

Base Command

hda-list-users

Input

Argument Name Description Required
user_id The ID of the user to retrieve details for. Optional
page The page number. Optional
page_size The number of users to return per page. Optional
limit The maximum number of users to return per page. Default is 50. Optional

Context Output

Path Type Description
HelpdeskAdvanced.User.ID string The user ID.
HelpdeskAdvanced.User.Email string The user’s email address.
HelpdeskAdvanced.User.FirstName string The user’s first name.
HelpdeskAdvanced.User.LastName string The user’s last name.
HelpdeskAdvanced.User.Phone string The user’s phone number.

Command example


#### Context Example

```json
{
    "HelpdeskAdvanced": {
        "User": [
            {
                "EMail": "Guest@example.com",
                "FirstName": null,
                "ID": "#GUEST#",
                "LastName": "Guest",
                "Phone": null
            }
        ]
    }
}

Human Readable Output

PAT HelpDeskAdvanced Users

Phone ID First Name Last Name E Mail
  #GUEST#   Guest Guest@example.com

hda-list-groups


List groups.

Base Command

hda-list-groups

Input

Argument Name Description Required
group_id The ID of the group to retrieve details for. Optional
page The page number. Optional
page_size The number of groups to return per page. Optional
limit The maximum number of groups to return per page. Default is 50. Optional

Context Output

Path Type Description
HelpdeskAdvanced.Group.ID string The group ID.
HelpdeskAdvanced.Group.ObjectTypeID string The object type ID of the group.
HelpdeskAdvanced.Group.Description string The group description.

Command example


#### Context Example

```json
{
    "HelpdeskAdvanced": {
        "Group": [
            {
                "Description": "Example",
                "ID": "G000C",
                "ObjectTypeID": "65"
            }
        ]
    }
}

Human Readable Output

PAT HelpDeskAdvanced Groups

Group ID Description Object Type ID
G000C Example 65

Configuration parameters

  • base_url — PAT Helpdesk URL (required)
  • credentials — Username (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (11)

  • hda-add-ticket-attachment

    Add an attachment to a ticket.

  • hda-add-ticket-comment

    Add a comment to a ticket.

  • hda-change-ticket-status

    Change the status of a ticket.

  • hda-create-ticket

    Create a new ticket.

  • hda-get-ticket-history

    Get ticket history.

  • hda-list-groups

    List groups.

  • hda-list-ticket-attachments

    List attachments for a ticket.

  • hda-list-ticket-priorities

    List ticket priorities.

  • hda-list-ticket-statuses

    List ticket statuses.

  • hda-list-tickets

    List tickets.

  • hda-list-users

    List users.

import contextlib
from collections.abc import Callable, Iterable
from json import JSONDecodeError
from pathlib import Path
from typing import Literal, NamedTuple

import demistomock as demisto
import more_itertools
from CommonServerPython import *  # noqa: F401
from requests import Response

from CommonServerUserPython import *  # noqa

DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
VENDOR = "HelpdeskAdvanced"
OPERATORS = ("eq", "gt", "lt", "ge", "le", "sw", "nw", "ne")
FILTER_CONDITION_REGEX = re.compile(rf"\A\"(?P<key>.*?)\" (?P<op>{'|'.join(OPERATORS)}) (?P<value>(?:\".*?\"|null))\Z")
PAT_DATE_FORMAT_REGEX = re.compile(r"/Date\((\d+)\)/")
HTML_H_TAG_REGEX = re.compile(r"<h\d>(.*?)<\/h\d>", flags=re.S)


class Field:
    _special_cases = {"unread": "UnRead", "id": "ID", "html": "HTML"}

    def __init__(self, demisto_name: str) -> None:
        title_parts = []
        for part in demisto_name.split("_"):
            if edge_case := Field._special_cases.get(part):
                title_parts.append(edge_case)
            else:
                title_parts.append(part.title())

        self.demisto_name = demisto_name  # lower_case
        self.hda_name = "".join(title_parts)  # PascalCase

    def __repr__(self) -> str:
        return self.hda_name


OBJECT_TYPE_ID = Field("object_type_id")
TICKET_STATUS_ID = Field("ticket_status_id")
TICKET_PRIORITY_ID = Field("ticket_priority_id")
TICKET_CLASSIFICATION_ID = Field("ticket_classification_id")
TICKET_TYPE_ID = Field("ticket_type_id")
OBJECT_DESCTIPTION = Field("object_description")
OBJECT_ENTITY = Field("object_entity")
CONTACT_ID = Field("contact_id")
SUBJECT = Field("subject")
PROBLEM = Field("problem")
SITE = Field("site")
ID = Field("id")
IS_NEW = Field("is_new")
EXPIRATION_DATE = Field("expiration_date")
FIRST_UPDATE_USER_ID = Field("first_update_user_id")
OWNER_USER_ID = Field("owner_user_id")
ASSIGNED_USER_ID = Field("assigned_user_id")
SOLUTION = Field("solution")
SERVICE_ID = Field("service_id")
LOCATION_ID = Field("location_id")
PROBLEM_HTML = Field("problem_html")
NEXT_EXPIRATION_ID = Field("next_expiration_id")
TASK_EFFORT = Field("task_effort")
SUPPLIER_ID = Field("supplier_id")
SOLUTION_HTML = Field("solution_html")
ESTIMATED_TASK_START_DATE = Field("estimated_task_start_date")
ACCOUNT_ID = Field("account_id")
MAIL_BOX_ID = Field("mail_box_id")
CLOSURE_DATE = Field("closure_date")
BILLED_TOKENS = Field("billed_tokens")
PARENT_TICKET_ID = Field("parent_ticket_id")
CUSTOMER_CONTRACT_ID = Field("customer_contract_id")
KNOWN_ISSUE = Field("known_issue")
LANGUAGE_ID = Field("language_id")
ASSET_ID = Field("asset_id")
DATE = Field("date")
URGENCY_ID = Field("urgency_id")
SCORE = Field("score")
ESTIMATED_TASK_DURATION = Field("estimated_task_duration")
SITE_UNREAD = Field("site_unread")
SOLICITS = Field("solicits")
CALENDAR_ID = Field("calendar_id")
LAST_EXPIRATION_DATE = Field("last_expiration_date")
NEXT_EXPIRATION_DATE = Field("next_expiration_date")
ASSIGNED_USER_OR_GROUP_ID = Field("next_user_or_group_id")
PARENT_OBJECT = Field("parent_object")
PARENT_OBJECT_ID = Field("parent_object_id")
TICKET_ID = Field("ticket_id")
TICKET_STATUS = Field("ticket_status")
TEXT_HTML = Field("text_html")
SITE_VISIBLE = Field("site_visible")
DESCRIPTION = Field("description")
TICKET_PRIORITY = Field("ticket_priority")
FILE_NAME = Field("file_name")
CONTENT_TYPE = Field("content_type")
LAST_UPDATE = Field("last_update")
OPERATION_TYPE_ID = Field("operation_type_id")
HISTORY_ID = Field("history_id")
OPERATION_DESCRIPTION = Field("operation_description")
UPDATE_DATE = Field("update_date")
FULL_NAME = Field("full_name")
USER_NAME = Field("user_name")

# Underscored fields are not real in HDA but used for the integration
_PRIORITY = Field("priority")
_TICKET_SOURCE = Field("ticket_source")
_OBJECT_ID = Field("object_id")
_TICKET = Field("ticket")
_ATTACHMENT_ID = Field("attachment_id")
_GROUP_ID = Field("group_id")
ID_DESCRIPTION_COLUMN_NAMES = str([field.hda_name for field in (ID, DESCRIPTION)])


class Filter(NamedTuple):
    key: str
    operator: str  # See OPERATORS
    value: str | None  # none only when the value is null and without quotes

    @staticmethod
    def _parse(string: str) -> "Filter":
        if not (match := FILTER_CONDITION_REGEX.match(string)):
            raise DemistoException(
                f'Cannot parse {string}. Expected a phrase of the form ["key" operator "value"] or ["key" operator null]'
            )
        return Filter(
            key=match["key"],
            operator=match["op"],
            value=None if ((value := match["value"]) == "null") else value.strip('"'),
        )

    @staticmethod
    def parse_list(strings: Iterable[str] | str) -> List["Filter"]:
        return [Filter._parse(string) for string in more_itertools.always_iterable(strings)]

    @property
    def dict(self):
        return {"property": self.key, "op": self.operator, "value": self.value}

    @staticmethod
    def dumps_list(filters: Union[Iterable["Filter"], "Filter"]) -> str:
        """
        Dumps a one or more Filter objects to a JSON string, as list (per API requirements).

        Args:
            filters (Union[Iterable[Filter], Filter]): The Filter object(s) to dump.

        Returns:
            str: The JSON string representation of the filters, as list.
        """
        return json.dumps([filter.dict for filter in more_itertools.always_iterable(filters, base_type=Filter)])


def safe_arg_to_number(argument: str | None, argument_name: str) -> int:
    # arg_to_number is typed as if it returns Optional[int], which causes mypy issues down the road.
    # this method solves them
    if (result := arg_to_number(argument)) is None:
        raise ValueError(f"cannot parse number from {argument_name}={argument}")
    return result


def create_params_dict(
    required_fields: tuple[Field, ...] = (),
    optional_fields: tuple[Field, ...] = (),
    **kwargs,
) -> dict[str, Any]:
    return (
        {field.hda_name: kwargs[field.demisto_name] for field in required_fields}
        | {field.hda_name: kwargs[field.demisto_name] for field in optional_fields if field.demisto_name in kwargs}
        | {
            # an exception to the rule
            PROBLEM_HTML.hda_name: kwargs["problem"],
        }
    )


class RequestNotSuccessfulError(DemistoException):
    def __init__(self, response: Response, attempted_action: str):
        json_response = {}
        with contextlib.suppress(JSONDecodeError):
            json_response = response.json()

        suffix = f": {description}." if (description := json_response.get("result", {}).get("desc")) else "."
        super().__init__(f"{attempted_action.capitalize()} failed{suffix}", res=response)


def map_id_to_description(response: dict) -> dict[str, str]:
    return {item[ID.hda_name]: item[DESCRIPTION.hda_name] for item in response["data"]}


def convert_response_dates(response: dict) -> dict:
    def convert_value(value: str) -> str | datetime:
        if isinstance(value, str) and value and (match := PAT_DATE_FORMAT_REGEX.match(value)):
            return datetime.fromtimestamp(
                int(match[1][:-3]),  # :-3 omits miliseconds
                tz=timezone.utc,
            ).strftime(DATETIME_FORMAT)
        return value

    def convert_recursively(
        value,
    ):  # no typing as the cases are complex and confuse mypy
        if isinstance(value, dict):
            return {k: convert_recursively(v) for k, v in value.items()}
        elif isinstance(value, list):
            return [convert_recursively(v) for v in value]
        return convert_value(value)

    return convert_recursively(response)


class Client(BaseClient):
    class Token(NamedTuple):
        refresh_token: str
        request_token: str
        expiry_utc: datetime

        @staticmethod
        def from_response(response: dict):
            return Client.Token(
                refresh_token=response["refreshToken"],
                request_token=response["requestToken"],
                expiry_utc=datetime.utcnow() + timedelta(seconds=response["expiresIn"]),
            )

        def write_to_integration_context(self) -> None:
            demisto.setIntegrationContext(
                demisto.getIntegrationContext()
                | {
                    "refresh_token": self.refresh_token,
                    "request_token": self.request_token,
                    "token_expiry_utc": str(self.expiry_utc),
                }
            )

    def http_request(
        self,
        url_suffix: str,
        method: Literal["GET", "POST"],
        attempted_action: str,
        require_success_true: bool = True,
        **kwargs,
    ) -> dict:
        response = self._http_request(
            method,
            url_suffix,
            resp_type="response",
            **kwargs,
        )
        try:
            response_body = json.loads(response.text)
            if (require_success_true or ("success" in response_body)) and (response_body["success"] is not True):
                # Some endpoints only contain the `success` key on failure ¯\_(ツ)_/¯
                raise RequestNotSuccessfulError(response, attempted_action)
            return response_body

        except JSONDecodeError:
            if error_parts := HTML_H_TAG_REGEX.findall(response.text):
                raise DemistoException(". ".join(error_parts), res=response)
            raise ValueError(f"API returned non-JSON response: {response.text}")

    def __init__(
        self,
        base_url: str,
        verify: bool,
        proxy: bool,
        username: str,
        password: str,
    ):
        super().__init__(base_url=base_url, verify=verify, proxy=proxy)
        self.token: Client.Token = self._reuse_or_create_token(username, password)
        self._headers = {"Authorization": f"Bearer {self.token.request_token}"}

    def _reuse_or_create_token(self, username: str, password: str) -> "Client.Token":
        # should only be called from __init__
        def generate_new_refresh_token() -> Client.Token:
            return Client.Token.from_response(
                self.http_request(
                    method="POST",
                    url_suffix="Authentication/LoginEx",
                    attempted_action="logging in using username and password",
                    params={"username": username, "password": password},
                    headers={"Content-Type": "multipart/form-data"},
                )
            )

        def generate_using_refresh_token(refresh_token: str) -> Client.Token:
            return Client.Token.from_response(
                self.http_request(
                    method="POST",
                    url_suffix="Authentication/RefreshToken",
                    params={"token": refresh_token},
                    attempted_action="generating request token using refresh token",
                    headers={"Content-Type": "multipart/form-data"},
                )
            )

        # Check integration context
        integration_context = demisto.getIntegrationContext()
        previous_refresh_token = integration_context.get("refresh_token")
        raw_previous_expiry_utc: str | None = integration_context.get("token_expiry_utc")

        # Do we need to log in again, or can we just refresh?
        if (
            raw_previous_expiry_utc
            and (token_expiry_utc := dateparser.parse(raw_previous_expiry_utc))
            and token_expiry_utc > (datetime.utcnow() + timedelta(seconds=5))
        ):
            try:
                demisto.debug("refresh token is valid, using it to generate a request token")
                return generate_using_refresh_token(previous_refresh_token)

            except RequestNotSuccessfulError:
                demisto.debug("failed using refresh token, getting a new one using username and password")
                return generate_new_refresh_token()

        else:
            demisto.debug("refresh token expired or missing, logging in with username and password. {token.expiry_utc=}")
            return generate_new_refresh_token()

    def create_ticket(self, **kwargs) -> dict:
        data = create_params_dict(
            required_fields=(
                OBJECT_TYPE_ID,
                TICKET_STATUS_ID,
                TICKET_PRIORITY_ID,
            ),
            optional_fields=(
                OBJECT_DESCTIPTION,
                TICKET_CLASSIFICATION_ID,
                TICKET_TYPE_ID,
                CONTACT_ID,
                SUBJECT,
                SITE,
            ),
            **kwargs,
        )

        return self.http_request(
            "WSC/Set",
            "POST",
            data={
                "entity": "Incident",
                "data": json.dumps(data),
            },
            attempted_action="creating ticket",
        )

    def list_tickets(self, **kwargs) -> dict:
        columns = str(
            [
                field.hda_name
                for field in (
                    OBJECT_DESCTIPTION,
                    OBJECT_ENTITY,
                    SOLUTION,
                    TICKET_CLASSIFICATION_ID,
                    SERVICE_ID,
                    PROBLEM_HTML,
                    CONTACT_ID,
                    NEXT_EXPIRATION_ID,
                    TASK_EFFORT,
                    ID,
                    SUPPLIER_ID,
                    SOLUTION_HTML,
                    IS_NEW,
                    EXPIRATION_DATE,
                    LOCATION_ID,
                    ESTIMATED_TASK_START_DATE,
                    FIRST_UPDATE_USER_ID,
                    ACCOUNT_ID,
                    MAIL_BOX_ID,
                    CLOSURE_DATE,
                    BILLED_TOKENS,
                    TICKET_TYPE_ID,
                    OWNER_USER_ID,
                    PARENT_TICKET_ID,
                    CUSTOMER_CONTRACT_ID,
                    LANGUAGE_ID,
                    KNOWN_ISSUE,
                    ASSET_ID,
                    DATE,
                    URGENCY_ID,
                    SCORE,
                    SUBJECT,
                    ESTIMATED_TASK_DURATION,
                    SOLICITS,
                    SITE,
                    CALENDAR_ID,
                    LAST_EXPIRATION_DATE,
                    SITE_UNREAD,
                    PROBLEM,
                    NEXT_EXPIRATION_DATE,
                    ASSIGNED_USER_OR_GROUP_ID,
                )
            ]
        )

        params: dict[str, str | list | int | None] = {
            "entity": "Ticket",
            "filter": None,
            "columnExpressions": columns,
            "columnNames": columns,
            "start": safe_arg_to_number(kwargs.get("start", 0), "start"),
            "limit": safe_arg_to_number(kwargs["limit"], "limit"),
        }

        if (raw_filter := kwargs.get("filter")) and (custom_filter := Filter.parse_list(raw_filter)):
            params["filter"] = Filter.dumps_list(custom_filter)

        if ticket_id := kwargs.get(TICKET_ID.demisto_name):
            if raw_filter:
                raise DemistoException("The ticket_id and filter arguments cannot be used at the same time.")
            # if only ticket_id is passed, use it to filter
            params["filter"] = Filter.dumps_list(Filter(ID.hda_name, "eq", ticket_id))

        return self.http_request(
            url_suffix="WSC/Projection",
            method="POST",
            attempted_action="listing tickets",
            params=params,
        )

    def add_ticket_attachment(self, entry_ids: list[str], **kwargs) -> dict:
        return self.http_request(
            url_suffix="Ticket/UploadNewAttachment",
            method="POST",
            attempted_action="uploading a new attachment",
            data={
                "entity": "Ticket",
                "entityID": kwargs["ticket_id"],
            },
            files=[
                (
                    f"TicketAttachment_{i+1}",
                    (
                        (file_entry := demisto.getFilePath(entry_id))["name"],
                        Path(file_entry["path"]).open("rb"),
                    ),
                )
                for i, entry_id in enumerate(entry_ids)
            ],
        )

    def list_ticket_attachments(self, **kwargs) -> dict:
        ticket_id = kwargs["ticket_id"]
        params = {
            "entity": "Attachments",
            "start": 0,
            "limit": safe_arg_to_number(kwargs["limit"], "limit"),
            "filter": Filter.dumps_list(
                (
                    Filter(PARENT_OBJECT.hda_name, "eq", _TICKET.hda_name),
                    Filter(PARENT_OBJECT_ID.hda_name, "eq", ticket_id),
                )
            ),
        }

        return self.http_request(
            url_suffix="/WSC/List",
            method="POST",
            params=params,
            attempted_action="listing ticket attachments",
        )

    def add_ticket_comment(self, **kwargs) -> dict:
        return self.http_request(
            url_suffix="WSC/Set",
            method="POST",
            data={
                "entity": "TicketConversationItem",
                "data": json.dumps(
                    {
                        TICKET_ID.hda_name: kwargs[TICKET_ID.demisto_name],
                        TEXT_HTML.hda_name: kwargs["comment"],
                        SITE_VISIBLE.hda_name: argToBoolean(kwargs[SITE_VISIBLE.demisto_name]),
                        OBJECT_TYPE_ID.hda_name: "90",  # hardcoded by design. 90 marks ObjectTypeIDField
                    }
                ),
            },
            attempted_action="adding ticket comment",
        )

    def list_ticket_statuses(self, limit: int | None) -> dict:
        data = {
            "entity": TICKET_STATUS.hda_name,
            "start": 0,
            "columnNames": ID_DESCRIPTION_COLUMN_NAMES,
            "columnExpressions": ID_DESCRIPTION_COLUMN_NAMES,
        }

        if limit is not None:
            # Sending without a limit _seems_ to return all records (was only able to test with 20)
            # XSOAR automatically adds this argument to the *command*, to not spam context with a long list of values.
            # However, this method is called internally in change_ticket_status, for which we *do* want all statuses.
            data["limit"] = limit

        return self.http_request(
            url_suffix="WSC/Projection",
            method="POST",
            attempted_action="listing ticket statuses",
            data=data,
        )

    def change_ticket_status(
        self,
        status_id: str,
        ticket_id: str,
        note: str | None = None,
    ) -> dict:
        allowed_id_values = tuple(item[ID.hda_name] for item in self.list_ticket_statuses(limit=None)["data"])

        if status_id not in allowed_id_values:
            raise DemistoException(f"Invalid {status_id=}. Possible values are {','.join(sorted(allowed_id_values))}")

        params = {
            "ticketID": ticket_id,
            "ticketStatusID": status_id,
        }

        if note:
            params["note"] = note

        return self.http_request(
            url_suffix="Ticket/DoChangeStatus",
            method="POST",
            attempted_action="changing ticket status",
            params=params,
        )

    def list_ticket_priorities(self) -> dict:
        return self.http_request(
            url_suffix="WSC/Projection",
            method="POST",
            attempted_action="listing ticket priorities",
            data={
                "entity": TICKET_PRIORITY.hda_name,
                "columnExpressions": ID_DESCRIPTION_COLUMN_NAMES,
                "columenNames": ID_DESCRIPTION_COLUMN_NAMES,
            },
        )

    def list_ticket_sources(self, limit: int) -> dict:
        return self.http_request(
            url_suffix="WSC/Projection",
            method="POST",
            attempted_action="listing ticket priorities",
            data={
                "entity": _TICKET_SOURCE.hda_name,
                "columnExpressions": ID_DESCRIPTION_COLUMN_NAMES,
                "columenNames": ID_DESCRIPTION_COLUMN_NAMES,
                "start": 0,
                "limit": limit,
            },
        )

    def get_ticket_history(self, ticket_id: str) -> dict:
        # Note: this endpoint has a different structure than the rest:
        #   1. no `data` (results are returned directly in the root).
        #   2. the `success` field only returned on failure. (hence require_succes_true=False)
        return self.http_request(
            url_suffix=f"Ticket/History?{_OBJECT_ID.hda_name}={ticket_id}",
            method="GET",
            attempted_action="getting ticket history",
            require_success_true=False,
        )

    def list_users(self, **kwargs):
        columns = str(
            [
                "ID",
                "User.FirstName",
                "User.LastName",
                "User.EMail",
                "User.Phone",
                "User.Mobile",
            ]
        )

        pagination = paginate(**kwargs)
        params = {
            "entity": "Users",
            "columnExpressions": columns,
            "columnNames": columns,
            "start": pagination.start,
            "limit": pagination.limit,
        }

        if user_ids := argToList(kwargs.get("user_id")):
            params["filter"] = Filter.dumps_list(tuple(Filter(ID.hda_name, "eq", id_) for id_ in user_ids))

        return self.http_request(
            url_suffix="WSC/Projection",
            method="POST",
            attempted_action="listing user(s)",
            params=params,
        )

    def list_groups(self, **kwargs) -> dict:
        columns = str([column.hda_name for column in (ID, DESCRIPTION, OBJECT_TYPE_ID)])

        params = {
            "entity": "UserGroup",
            "columnExpressions": columns,
            "columnNames": columns,
            "start": (pagination := paginate(**kwargs)).start,
            "limit": pagination.limit,
        }

        if group_id := kwargs.get("group_id"):
            params["filter"] = Filter.dumps_list(Filter(ID.hda_name, "eq", group_id))

        return self.http_request(
            url_suffix="WSC/Projection",
            method="POST",
            attempted_action="listing group(s)",
            params=params,
        )


class PaginateArgs(NamedTuple):
    start: int
    limit: int


def paginate(**kwargs) -> PaginateArgs:
    limit = safe_arg_to_number(kwargs["limit"], "limit")

    page = kwargs.get("page")
    page_size = kwargs.get("page_size")

    none_arg_count = sum((page is None, page_size is None))

    if none_arg_count == 1:
        raise DemistoException(
            "To paginate, provide both `page` and `page_size` arguments."
            "To only get the first n results (without paginating), use the `limit` argument."
        )

    if none_arg_count == 2:  # neither page nor page_size provided
        return PaginateArgs(start=0, limit=limit)

    # here none_arg_count = 0, meaning both were provided
    page = safe_arg_to_number(page, "page")
    page_size = safe_arg_to_number(page_size, "page_size")

    return PaginateArgs(
        start=page * page_size,
        limit=min(limit, page_size),
    )


def pat_table_to_markdown(
    title: str,
    output: dict | list[dict],
    fields: tuple[Field, ...] | None,
    field_replacements: dict[Field, Field] | None = None,
    **kwargs,
) -> str:
    def replace_fields(item: Any):
        string_key_replacements = {k.hda_name: v.hda_name for k, v in (field_replacements or {}).items()}
        if isinstance(item, dict):
            return {string_key_replacements.get(key, key): value for key, value in item.items()}
        elif isinstance(item, list):
            return [replace_fields(v) for v in item]
        return item

    def filter_fields(item: dict | list[dict]) -> dict | list[dict]:
        def _filter_dict(dictionary: dict) -> dict:
            return {
                field.hda_name: dictionary[field.hda_name]
                # mypy doesn't notice fields can not be None here
                for field in fields  # type:ignore[union-attr]
                if field.hda_name in dictionary
            }

        if isinstance(item, dict):
            return _filter_dict(item)
        elif isinstance(item, list):
            # mypy is confused by the type union
            return [filter_fields(list_item) for list_item in item]  # type:ignore[misc]
        raise TypeError(f"cannot filter {type(item)}, expected dict|list[dict]")

    if field_replacements:
        output = replace_fields(output)

    if fields is not None:
        output = filter_fields(output)

    return tableToMarkdown(
        name=title,
        t=output,
        headerTransform=pascalToSpace,
        sort_headers=False,
        **kwargs,
    )


def create_ticket_command(client: Client, args: dict) -> CommandResults:
    raw_response = client.create_ticket(**args)
    response = convert_response_dates(raw_response)

    response_for_human_readable = response.copy()["data"]

    if not response_for_human_readable.get(SOLUTION.hda_name):
        # do not show empty or missing `Solution` value
        response_for_human_readable.pop(SOLUTION.hda_name, None)

    return CommandResults(
        outputs_prefix=f"{VENDOR}.Ticket",
        outputs_key_field=ID.hda_name,
        outputs=response,
        raw_response=raw_response,
        readable_output=pat_table_to_markdown(
            title="Ticket Created",
            output=response_for_human_readable,
            fields=(
                TICKET_ID,
                OBJECT_DESCTIPTION,
                OBJECT_ENTITY,
                SOLUTION,
                TICKET_CLASSIFICATION_ID,
                IS_NEW,
                EXPIRATION_DATE,
                FIRST_UPDATE_USER_ID,
                OWNER_USER_ID,
                DATE,
                ASSIGNED_USER_ID,
            ),
        ),
    )


def list_tickets_command(client: Client, args: dict) -> CommandResults:
    raw_response = client.list_tickets(**args)
    response = convert_response_dates(raw_response)

    return CommandResults(
        outputs=response["data"],
        raw_response=raw_response,
        outputs_prefix=f"{VENDOR}.Ticket",
        outputs_key_field=ID.hda_name,
        readable_output=pat_table_to_markdown(
            title="Tickets",
            output=response["data"],
            fields=(
                TICKET_ID,  # Replaced from ID
                SUBJECT,
                SOLUTION,
                DATE,
                SERVICE_ID,
                PROBLEM,
                CONTACT_ID,
                OWNER_USER_ID,
                ACCOUNT_ID,
            ),
            field_replacements={ID: TICKET_ID},
        ),
    )


def add_ticket_attachment_command(client: Client, args: dict) -> CommandResults:
    entry_ids = argToList(args.pop("entry_id", ()))
    ticket_id = args["ticket_id"]
    response = client.add_ticket_attachment(entry_ids, **args)
    return CommandResults(
        readable_output=f"Added Attachment ID {response['data']['attachmentID']} to ticket ID {ticket_id}"
        if len(entry_ids) == 1
        else f"Added {len(entry_ids)} attachments to ticket ID {ticket_id}",  # API only returns the last attachment ID
        raw_response=response,
    )


def list_ticket_attachments_command(client: Client, args: dict) -> CommandResults:
    response = client.list_ticket_attachments(**args)
    response = convert_response_dates(response)

    return CommandResults(
        outputs=response["data"],
        outputs_prefix=f"{VENDOR}.Ticket.Attachment",
        outputs_key_field=ID.hda_name,
        readable_output=pat_table_to_markdown(
            title=f"Attachments of {args['ticket_id']}",
            output=response["data"],
            fields=(
                ID,
                FILE_NAME,
                LAST_UPDATE,
                DESCRIPTION,
                OBJECT_DESCTIPTION,
                FIRST_UPDATE_USER_ID,
                OBJECT_ENTITY,
                CONTENT_TYPE,
            ),
            field_replacements={ID: _ATTACHMENT_ID},
        ),
        raw_response=response,
    )


def list_ticket_statuses_command(client: Client, args: dict) -> CommandResults:
    response = client.list_ticket_statuses(limit=safe_arg_to_number(args["limit"], "limit"))
    response = convert_response_dates(response)

    return CommandResults(
        outputs=response["data"],
        outputs_prefix=f"{VENDOR}.{TICKET_STATUS.hda_name}",
        raw_response=response,
    )


def add_ticket_comment_command(client: Client, args: dict) -> CommandResults:
    return CommandResults(
        readable_output=f"Comment was succesfully added to {args['ticket_id']}",
        raw_response=client.add_ticket_comment(**args),
    )


def change_ticket_status_command(client: Client, args: dict) -> CommandResults:
    response = client.change_ticket_status(**args)
    return CommandResults(
        readable_output=f"Changed status of ticket {args['ticket_id']} to {args['status_id']} successfully.",
        raw_response=response,
    )


def list_ticket_priorities_command(client: Client, _: dict) -> CommandResults:
    response = client.list_ticket_priorities()
    response = convert_response_dates(response)

    return CommandResults(
        outputs=response["data"],
        outputs_prefix=f"{VENDOR}.{_PRIORITY.hda_name}",
        readable_output=tableToMarkdown("HDA Ticket Priorities", response["data"]),
        raw_response=response,
    )


def list_ticket_sources_command(client: Client, args: dict) -> CommandResults:
    limit = safe_arg_to_number(args["limit"], "limit")

    response = client.list_ticket_sources(limit)
    response = convert_response_dates(response)

    outputs = map_id_to_description(response["data"])
    return CommandResults(
        outputs=outputs,
        outputs_prefix=f"{VENDOR}.{_TICKET_SOURCE.hda_name}",
        readable_output=tableToMarkdown(
            name="PAT HelpdeskAdvanced Ticket Sources",
            t=outputs,
            headers=["Source ID", "Source Description"],
        ),
        raw_response=response,
    )


def get_ticket_history_command(client: Client, args: dict) -> CommandResults:
    raw_response = client.get_ticket_history(ticket_id := args["ticket_id"])

    response = convert_response_dates(raw_response)
    response = [value | {TICKET_ID.hda_name: ticket_id} for value in response]
    return CommandResults(
        outputs=response,
        outputs_prefix=f"{VENDOR}.TicketHistory",
        outputs_key_field=HISTORY_ID.hda_name,
        raw_response=raw_response,
        readable_output=pat_table_to_markdown(
            title=f"Ticket History: {ticket_id}",
            output=response,
            fields=None,
            removeNull=True,
            is_auto_json_transform=True,
        ),
    )


def list_users_command(client: Client, args: dict) -> CommandResults:
    response = client.list_users(**args)
    response = convert_response_dates(response)

    if not (data := response["data"]):
        return CommandResults(readable_output="No data returned")

    data = [
        # removing the User. prefix - both for visual and context path (avoiding duplication in HelpdeskAdvanced.User.User.EMail)
        {key.removeprefix("User."): value for key, value in user.items()}
        for user in data
    ]
    return CommandResults(
        outputs=data,
        outputs_prefix=f"{VENDOR}.User",
        outputs_key_field=ID.hda_name,
        raw_response=response,
        readable_output=pat_table_to_markdown(
            title="PAT HelpDeskAdvanced Users",
            output=data,
            fields=None,
        ),
    )


def list_groups_command(client: Client, args: dict) -> CommandResults:
    response = client.list_groups(**args)
    response = convert_response_dates(response)

    if not (data := response["data"]):
        return CommandResults(readable_output="No data returned")

    return CommandResults(
        outputs=data,
        outputs_prefix=f"{VENDOR}.Group",
        outputs_key_field=ID.hda_name,
        raw_response=response,
        readable_output=pat_table_to_markdown(
            title="PAT HelpDeskAdvanced Groups",
            output=data,
            fields=(_GROUP_ID, DESCRIPTION, OBJECT_TYPE_ID),
            field_replacements={ID: _GROUP_ID},
        ),
    )


commands: dict[str, Callable] = {
    "hda-create-ticket": create_ticket_command,
    "hda-list-tickets": list_tickets_command,
    "hda-list-ticket-attachments": list_ticket_attachments_command,
    "hda-add-ticket-attachment": add_ticket_attachment_command,
    "hda-add-ticket-comment": add_ticket_comment_command,
    "hda-list-ticket-statuses": list_ticket_statuses_command,
    "hda-change-ticket-status": change_ticket_status_command,
    "hda-list-ticket-priorities": list_ticket_priorities_command,
    "hda-list-ticket-sources": list_ticket_sources_command,
    "hda-get-ticket-history": get_ticket_history_command,
    "hda-list-users": list_users_command,
    "hda-list-groups": list_groups_command,
}


def main() -> None:
    demisto.debug(f"Command being called is {demisto.command()}")
    params = demisto.params()

    try:
        client = Client(
            base_url=urljoin(params["base_url"].removesuffix("HDAPortal"), "HDAPortal"),
            username=params["credentials"]["identifier"],
            password=params["credentials"]["password"],
            verify=not params["insecure"],
            proxy=params["proxy"],
        )

        if (command := demisto.command()) == "test-module":
            client.list_ticket_statuses(limit=1)
            result = "ok"

        elif command in commands:
            result = commands[command](client, demisto.args())

        else:
            raise NotImplementedError

        return_results(result)

    except Exception as e:
        return_error(
            "\n".join(
                (
                    f"Failed to execute {demisto.command()}.",
                    f"Error: {e!s}",
                    traceback.format_exc(),
                )
            )
        )


""" ENTRY POINT """


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