ServiceDeskPlus
Use this integration to manage on-premises and cloud Service Desk Plus requests. The integration allows you to create, update, and delete requests, assign groups and technicians to requests, and link/unlink requests and modify their resolution.
Case Management · Manage Engine Service Desk Plus
Details
| ID | ServiceDeskPlus |
|---|---|
| Provider | Zoho Corporation |
| Category | Case Management |
| From Version | 5.0.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM |
README
IT Service Management
Configure ServiceDeskPlus in Cortex
- In Cortex XSOAR, navigate to Settings > Integrations > Instances. In Cortex XSIAM, navigate to Settings > Automation & Feed Integrations.
- Search for ServiceDeskPlus.
- Click Add instance to create and configure a new integration instance.
Instance Creation Flow for Cloud
To create an instance of the Service Desk Plus integration, you need to get a Client ID, Client Secret, and Refresh Token.
Follow the next steps to create an instance:
- Select the data center in which your data resides.
- Register the app using ZOHO App Registration. Copy the Client ID and Client Secret, then paste them into the integration instance and click Done.
- In the registered app, select the Generate Code tab and define the scopes for the app.
- In the War Room or Playground, run the !service-desk-plus-generate-refresh-token command and paste the generated code into the Refresh Token parameter.
- Click the Test button to validate the instance.
- If the test is successful, click Save to save the instance.
Instance Creation Flow for On-Premise
To create an instance of the Service Desk Plus integration, you need to get a On-Premises Server URL and a Technician Key.
Follow the next steps to create an instance:
- Enter the On-Premise Server URL.
- Enter the Technician Key.
- Click the Test button to validate the instance.
- If the test is successful, click Save to save the instance.
NOTES
- For more details about the app authorization process refer to App Authorization
- The code generated in the app is only valid for a limited time.
- In order to avoid repeating this process, the created Refresh Token should be saved for future use.
- For more details about generating a technician key please refer to the help documentation

| Parameter | Description | Required |
|---|---|---|
| server_url | Data Center Location: Select the domain location that is applicable for you application | True |
| technician_key | Technician Key | False |
| server_url_on_premise | On-Premise Server URL | False |
| client_id | Client ID | False |
| client_secret | Client Secret | False |
| refresh_token | Refresh Token | False |
| isFetch | Fetch incidents | False |
| incidentType | Incident type | False |
| insecure | Trust any certificate (not secure) | False |
| proxy | Use system proxy settings | False |
| fetch_status | The status of the requests that should be fetched. Multiple status can be entered, separated by a comma. | False |
| fetch_time | First fetch time range (<number> <time unit>, e.g., 1 hour, 30 minutes) | False |
| fetch_limit | The maximum number of incidents that should be fetched each time | False |
| fetch_filter | Use this field to filter the incidents that are being fetched according to any of the request properties. Please see additional information for exact filter format. Overrides the status filter, if given. | False |
- Click Test to validate the URLs, token, and connection.
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.
service-desk-plus-requests-list
View the details of requests. If no parameters are given the details of all requests will be shown.
Base Command
service-desk-plus-requests-list
Input
| Argument Name | Description | Required |
|---|---|---|
| request_id | The unique request id of the request that should be shown. | Optional |
| start_index | Use this to get a list of tasks starting from this index. e.g: 6 |
Optional |
| page_size | Use this to mention the number of requests that needs to be returned. e.g: 15. By default, will return only the first 10 requests. |
Optional |
| search_fields | The column name and value to be searched for in the format of a json object. e.g {“subject”:“Change like this”,“priority.name”:“High”} | Optional |
| filter_by | The name of the filter that should be used. e.g {“name”:“My_Open”} | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ServiceDeskPlus.Request.Requester | Unknown | The requester of the request |
| ServiceDeskPlus.Request.CreatedTime | Date | The time the request was created |
| ServiceDeskPlus.Request.Template | Unknown | The template that was used to create the request |
| ServiceDeskPlus.Request.DisplayId | String | The display id of the request |
| ServiceDeskPlus.Request.Id | String | The unique id of the request |
| ServiceDeskPlus.Request.Subject | String | The subject of the request |
| ServiceDeskPlus.Request.Technician | String | The technician that was assigned to the request |
| ServiceDeskPlus.Request.Status | String | The status of the request |
| ServiceDeskPlus.Request.DueByTime | Date | The due date of the request |
| ServiceDeskPlus.Request.Group | String | The group to which the request belongs |
| ServiceDeskPlus.Request.IsServiceRequest | Boolean | Indicates whether the request is a service request or not |
| ServiceDeskPlus.Request.CancellationRequested | Boolean | Indicates whether a cancellation was requested |
| ServiceDeskPlus.Request.HasNotes | Boolean | Indicates whether the command has notes or not |
Command Example
!service-desk-plus-requests-list start_index=32 page_size=2
Context Example
{
"ServiceDeskPlus": {
"Request": [
{
"CancellationRequested": false,
"CreatedTime": "2020-06-30T15:27:33.000Z",
"DisplayId": "172",
"HasNotes": false,
"Id": "123640000000279007",
"IsServiceRequest": false,
"Requester": {
"email_id": null,
"is_technician": false,
"sms_mail": null,
"phone": null,
"name": "First Last",
"mobile": null,
"id": "123640000000244019",
"photo_url": "https://contacts.zoho.com/file?exp=10&ID=-1&t=user&height=60&width=60",
"is_vip_user": false,
"department": null
},
"Status": "Open",
"Subject": "Update Existing Request",
"Template": {
"id": "123640000000006655",
"name": "Default Request"
}
},
{
"CancellationRequested": false,
"CreatedTime": "2020-07-01T08:03:12.000Z",
"DisplayId": "180",
"HasNotes": false,
"Id": "123640000000281015",
"IsServiceRequest": false,
"Requester": {
"department": null,
"email_id": null,
"id": "123640000000244019",
"is_technician": false,
"is_vip_user": false,
"mobile": null,
"name": "First Last",
"phone": null,
"photo_url": "https://contacts.zoho.com/file?exp=10&ID=-1&t=user&height=60&width=60",
"sms_mail": null
},
"Status": "Open",
"Subject": "Create New Request",
"Template": {
"id": "123640000000006655",
"name": "Default Request"
}
}
]
}
}
Human Readable Output
Requests
CreatedTime Id Requester Status Subject 2020-06-30T15:27:33.000Z 123640000000279007 First Last Open Update Existing Request 2020-07-01T08:03:12.000Z 123640000000281015 First Last Open Create New Request
service-desk-plus-request-delete
Deletes the request with the specified ID. To delete multiple IDs, pass a comma-separated list (string) of the IDs of the requests.
Base Command
service-desk-plus-request-delete
Input
| Argument Name | Description | Required |
|---|---|---|
| request_id | The id of the request that should be deleted | Required |
Context Output
There is no context output for this command.
Command Example
!service-desk-plus-request-delete request_id=123640000000279019
Human Readable Output
Successfully deleted request(s) 123640000000279019
service-desk-plus-request-create
Create new requests
Base Command
service-desk-plus-request-create
Input
| Argument Name | Description | Required |
|---|---|---|
| subject | Subject of this request | Required |
| description | Description of this request | Optional |
| request_type | Type of this request. Choose one of the listed options or provide a dictionary representing a request_type object. | Optional |
| impact_details | Description about the impact of the request; must not exceed 250 characters. | Optional |
| impact | Impact of this request. Choose one of the listed options or provide a dictionary representing an impact object. | Optional |
| status | Indicates the current status of this request. Choose one of the listed options or provide a dictionary representing a status object. | Optional |
| mode | The mode in which the request is created. Choose one of the listed options or provide a dictionary representing a mode object. | Optional |
| level | Level of the request. Choose one of the listed options or provide a dictionary representing a level object. | Optional |
| urgency | Urgency of the request. Choose one of the listed options or provide a dictionary representing an urgency object. | Optional |
| priority | Priority of the request. Choose one of the listed options or provide a dictionary representing a priority object. | Optional |
| service_category | Service category to which this request belongs. String representing the category’s name. | Optional |
| requester | Indicates the requester of this request. Type the name of the requester as a string or a dictionary representing a requester object. | Optional |
| assets | Array of asset objects associated to this request | Optional |
| site | Denotes the site to which this request belongs. Type a site name or provide a dictionary representing a site object. | Optional |
| group | Group to which this request belongs. Type the name of the group or a dictionary representing a group object. | Optional |
| technician | Technician assigned to this request. Type the name of the technician or a dictionary representing a technician object. | Optional |
| category | Category to which the request belongs. Fill in the name of the category or a dictionary representing a category object. | Optional |
| subcategory | Subcategory to which this request belongs. Fill in the name of the subcategory or a dictionary representing a subcategory object. | Optional |
| item | Item of this request. Fill in the item’s name or a dictionary representing an item object. | Optional |
| email_ids_to_notify | Array of Email ids, which needs to be notified about the happenings of this request | Optional |
| is_fcr | Boolean value indicating if the request has been marked as First Call Resolution | Optional |
| resources | Holds the resource data mapped to the request | Optional |
| udf_fields | Holds udf fields’ values associated with the request. Input format: A string of the form “key1:value1,key2:value2” or a dictionary of the form “{‘key1’:’val1’,’key2’:’val2’}” | Optional |
Note:
Fields that represent an object can be filled either by providing the name only or the entire object. For example, the technician parameter can be defined either by filling
technician=”First Last” or technician=”{‘name’:’First Last’, ‘id’:’12345’, ‘email_id’:’email@paloaltonetworks.com’}”
Context Output
| Path | Type | Description |
|---|---|---|
| ServiceDeskPlus.Request.Requester | Unknown | The requester of the request |
| ServiceDeskPlus.Request.CreatedTime | Date | The time the request was created |
| ServiceDeskPlus.Request.Template | Unknown | The template that was used to create the request |
| ServiceDeskPlus.Request.DisplayId | String | The display id of the request |
| ServiceDeskPlus.Request.Id | String | The unique id of the request |
| ServiceDeskPlus.Request.Subject | String | The subject of the request |
| ServiceDeskPlus.Request.Technician | String | The technician that was assigned to the request |
| ServiceDeskPlus.Request.Status | String | The status of the request |
| ServiceDeskPlus.Request.DueByTime | Date | The due date of the request |
| ServiceDeskPlus.Request.Group | String | The group to which the request belongs |
| ServiceDeskPlus.Request.IsServiceRequest | Boolean | Indicates whether the request is a service request or not |
| ServiceDeskPlus.Request.CancellationRequested | Boolean | Indicates whether a cancellation was requested |
| ServiceDeskPlus.Request.HasNotes | Boolean | Indicates whether the command has notes or not |
Command Example
!service-desk-plus-request-create subject="Create New Request" requester="{'name':'First Last'}" impact_details="Workflow affected"
Context Example
{
"ServiceDeskPlus": {
"Request": {
"CancellationRequested": false,
"CreatedBy": {
"department": null,
"email_id": "email@paloaltonetworks.com",
"id": "123640000000142582",
"is_technician": true,
"is_vip_user": false,
"mobile": null,
"name": "First Last",
"phone": null,
"photo_url": "https://contacts.zoho.com/file?exp=10&ID=712874208&t=user&height=60&width=60",
"sms_mail": null
},
"CreatedTime": "2020-07-01T08:17:06.000Z",
"DisplayId": "181",
"HasAttachments": false,
"HasLinkedRequests": false,
"HasNotes": false,
"HasProblem": false,
"HasProject": false,
"HasRequestInitiatedChange": false,
"Id": "123640000000276021",
"ImpactDetails": "Workflow affected",
"IsEscalated": false,
"IsFcr": false,
"IsFirstResponseOverdue": false,
"IsOverdue": false,
"IsRead": false,
"IsReopened": false,
"IsServiceRequest": false,
"IsTrashed": false,
"LastUpdatedTime": "2020-07-01T08:17:06.000Z",
"Requester": {
"department": null,
"email_id": null,
"id": "123640000000244019",
"is_technician": false,
"is_vip_user": false,
"mobile": null,
"name": "First Last",
"phone": null,
"photo_url": "https://contacts.zoho.com/file?exp=10&ID=-1&t=user&height=60&width=60",
"sms_mail": null
},
"Status": "Open",
"Subject": "Create New Request",
"Template": {
"id": "123640000000006655",
"name": "Default Request"
},
"TimeElapsed": "0",
"UdfFields": {
"udf_char2": "def test"
},
"UnrepliedCount": 0
}
}
}
Human Readable Output
Service Desk Plus request was successfully created
CreatedTime Id Requester Status Subject 2020-07-01T08:17:06.000Z 123640000000276021 First Last Open Create New Request
service-desk-plus-request-update
Update the request with the given request id.
Base Command
service-desk-plus-request-update
Input
| Argument Name | Description | Required |
|---|---|---|
| request_id | The ID of this request | Required |
| subject | Subject of this request | Optional |
| description | Description of this request | Optional |
| request_type | Type of this request. Choose one of the listed options or provide a dictionary representing a request_type object. | Optional |
| impact | Impact of this request. Choose one of the listed options or provide a dictionary representing an impact object. | Optional |
| status | Indicates the current status of this request. Choose one of the listed options or provide a dictionary representing a status object. | Optional |
| mode | The mode in which the request is created. Choose one of the listed options or provide a dictionary representing a mode object. | Optional |
| level | Level of the request. Choose one of the listed options or provide a dictionary representing a level object. | Optional |
| urgency | Urgency of the request. Choose one of the listed options or provide a dictionary representing an urgency object. | Optional |
| priority | Priority of the request. Choose one of the listed options or provide a dictionary representing a priority object. | Optional |
| service_category | Service category to which this request belongs. String representing the category’s name. | Optional |
| requester | Indicates the requester of this request. Type the name of the requester as a string or a dictionary representing a requester object. | Optional |
| assets | Array of asset objects associated to this request | Optional |
| site | Denotes the site to which this request belongs. Type a site name or provide a dictionary representing a site object. | Optional |
| group | Group to which this request belongs. Type the name of the group or a dictionary representing a group object. | Optional |
| technician | Technician assigned to this request. Type the name of the technician or a dictionary representing a technician object. | Optional |
| category | Category to which the request belongs. Fill in the name of the category or a dictionary representing a category object. | Optional |
| subcategory | Subcategory to which this request belongs. Fill in the name of the subcategory or a dictionary representing a subcategory object. | Optional |
| item | Item of this request. Fill in the item’s name or a dictionary representing an item object. | Optional |
| email_ids_to_notify | Array of Email ids, which needs to be notified about the happenings of this request | Optional |
| is_fcr | Boolean value indicating if the request has been marked as First Call Resolution | Optional |
| resources | Holds the resource data mapped to the request | Optional |
| udf_fields | Holds udf fields’ values associated with the request. Input format: A string of the form “key1:value1,key2:value2” or a dictionary of the form “{‘key1’:’val1’,’key2’:’val2’}” | Optional |
| update_reason | The reason for updating this request | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ServiceDeskPlus.Request.Requester | Unknown | The requester of the request |
| ServiceDeskPlus.Request.CreatedTime | Date | The time the request was created |
| ServiceDeskPlus.Request.Template | Unknown | The template that was used to create the request |
| ServiceDeskPlus.Request.DisplayId | String | The display id of the request |
| ServiceDeskPlus.Request.Id | String | The unique id of the request |
| ServiceDeskPlus.Request.Subject | String | The subject of the request |
| ServiceDeskPlus.Request.Technician | String | The technician that was assigned to the request |
| ServiceDeskPlus.Request.Status | String | The status of the request |
| ServiceDeskPlus.Request.DueByTime | Date | The due date of the request |
| ServiceDeskPlus.Request.Group | String | The group to which the request belongs |
| ServiceDeskPlus.Request.IsServiceRequest | Boolean | Indicates whether the request is a service request or not |
| ServiceDeskPlus.Request.CancellationRequested | Boolean | Indicates whether a cancellation was requested |
| ServiceDeskPlus.Request.HasNotes | Boolean | Indicates whether the command has notes or not |
Command Example
!service-desk-plus-request-update request_id=123640000000284007 subject="Update Existing Request"
Context Example
{
"ServiceDeskPlus": {
"Request": {
"CancellationRequested": false,
"CreatedBy": {
"department": null,
"email_id": "email@paloaltonetworks.com",
"id": "123640000000142582",
"is_technician": true,
"is_vip_user": false,
"mobile": null,
"name": "First Last",
"phone": null,
"photo_url": "https://contacts.zoho.com/file?exp=10&ID=712874208&t=user&height=60&width=60",
"sms_mail": null
},
"CreatedTime": "2020-07-01T07:41:14.000Z",
"DisplayId": "177",
"Group": {
"deleted": false,
"id": "123640000000006681",
"name": "Network",
"site": null
},
"HasAttachments": false,
"HasLinkedRequests": false,
"HasNotes": false,
"HasProblem": false,
"HasProject": false,
"HasRequestInitiatedChange": false,
"Id": "123640000000284007",
"IsEscalated": false,
"IsFcr": false,
"IsFirstResponseOverdue": false,
"IsOverdue": false,
"IsRead": false,
"IsReopened": true,
"IsServiceRequest": false,
"IsTrashed": false,
"LastUpdatedTime": "2020-07-01T08:17:08.000Z",
"Requester": {
"department": null,
"email_id": null,
"id": "123640000000244019",
"is_technician": false,
"is_vip_user": false,
"mobile": null,
"name": "First Last",
"phone": null,
"photo_url": "https://contacts.zoho.com/file?exp=10&ID=-1&t=user&height=60&width=60",
"sms_mail": null
},
"Resolution": {
"content": "Resolution Content"
},
"RespondedTime": "2020-07-01T08:12:31.000Z",
"Status": "Open",
"Subject": "Update Existing Request",
"Template": {
"id": "123640000000006655",
"name": "Default Request"
},
"TimeElapsed": "0",
"UdfFields": {
"udf_char2": "def test"
},
"UnrepliedCount": 0
}
}
}
Human Readable Output
Service Desk Plus request was successfully updated
CreatedTime Id Requester Status Subject 2020-07-01T07:41:14.000Z 123640000000284007 First Last Open Update Existing Request
service-desk-plus-request-assign
Assigns the request with the given request id to a technician/group
Base Command
service-desk-plus-request-assign
Input
| Argument Name | Description | Required |
|---|---|---|
| request_id | The id of the request that should be assigned | Required |
| technician | The name of the technician that should be assigned to the request | Optional |
| group | The name of the group that should be assigned to the request | Optional |
Context Output
There is no context output for this command.
Command Example
!service-desk-plus-request-assign request_id=123640000000284007 group="Network"
Context Example
{}
Human Readable Output
Service Desk Plus request 123640000000284007 was successfully assigned
service-desk-plus-request-pickup
Allows the technician to pickup the request with the given request id on his name.
Base Command
service-desk-plus-request-pickup
Input
| Argument Name | Description | Required |
|---|---|---|
| request_id | The id of the request that should be picked up | Required |
Context Output
There is no context output for this command.
Command Example
!service-desk-plus-request-pickup request_id=123640000000279001
Context Example
{}
Human Readable Output
Service Desk Plus request 123640000000279001 was successfully picked up
service-desk-plus-linked-request-list
Gets a list with all the linked requests under a request
Base Command
service-desk-plus-linked-request-list
Input
| Argument Name | Description | Required |
|---|---|---|
| request_id | The request for which the linked requests are requested | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| ServiceDeskPlus.Request.LinkRequests.Comments | Unknown | The comment that was added to the linked request |
| ServiceDeskPlus.Request.LinkRequests.LinkedRequest | Unknown | The linked request information |
Command Example
!service-desk-plus-linked-request-list request_id=123640000000284007
Context Example
{
"ServiceDeskPlus": {
"Request": {
"LinkRequests": [
{
"LinkedRequest": {
"display_id": "179",
"id": "123640000000288001",
"subject": "Request for List",
"udf_fields": {
"udf_char1": null,
"udf_char2": "def test",
"udf_char3": null
}
}
}
]
}
}
}
Human Readable Output
Linked requests to request 123640000000284007
LinkedRequest subject: Request for List
id: 123640000000288001
udf_fields: {“udf_char1”: null, “udf_char2”: “def test”, “udf_char3”: null}
display_id: 179
service-desk-plus-request-resolution-add
Adds a resolution to the given request
Base Command
service-desk-plus-request-resolution-add
Input
| Argument Name | Description | Required |
|---|---|---|
| request_id | The id of the request for which the resolution should be added | Required |
| resolution_content | The content of the resolution that should be added to the request | Optional |
| add_to_linked_requests | A boolean value indicating whether the same resolution should be added to all linked request of the request | Optional |
Context Output
There is no context output for this command.
Command Example
!service-desk-plus-request-resolution-add request_id=123640000000284007 resolution_content="Resolution Content"
Context Example
{}
Human Readable Output
Resolution was successfully added to 123640000000284007
service-desk-plus-request-resolutions-list
Gets the resolution to the given request
Base Command
service-desk-plus-request-resolutions-list
Input
| Argument Name | Description | Required |
|---|---|---|
| request_id | The id of the request for which the resolution is desired | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| ServiceDeskPlus.Request.Resolution.Content | Unknown | The content of the resolution of the request |
| ServiceDeskPlus.Request.Resolution.SubmittedBy | Unknown | The details of who submitted the resolution |
| ServiceDeskPlus.Request.Resolution.SubmittedOn | Unknown | The date the resolution was submitted |
| ServiceDeskPlus.Request.Resolution.ResolutionAttachments | Unknown | The attachments that were added to the resolution |
Command Example
!service-desk-plus-request-resolutions-list request_id=123640000000284007
Context Example
{
"ServiceDeskPlus": {
"Request": {
"Resolution": {
"Content": "Resolution Content",
"SubmittedBy": {
"department": null,
"email_id": "email@paloaltonetworks.com",
"id": "123640000000142582",
"is_technician": true,
"is_vip_user": false,
"mobile": null,
"name": "First Last",
"phone": null,
"photo_url": "https://contacts.zoho.com/file?exp=10&ID=712874208&t=user&height=60&width=60",
"sms_mail": null
},
"SubmittedOn": "2020-07-01T08:12:31.000Z"
}
}
}
}
Human Readable Output
Resolution of request 123640000000284007
Content SubmittedBy SubmittedOn Resolution Content First Last 2020-07-01T08:12:31.000Z
service-desk-plus-generate-refresh-token
This function generates the refresh token that should be used in the instance configurations
Base Command
service-desk-plus-generate-refresh-token
Input
| Argument Name | Description | Required |
|---|---|---|
| code | The code received when creating the application | Required |
Context Output
There is no context output for this command.
Command Example
!service-desk-plus-generate-refresh-token code=1000.ceb0e1b783c1e9647171e46a92bb444c.61adef3ee9d50c4a83e505ceee06d5b4
Context Example
{}
Human Readable Output
###Refresh Token: 1000.75a65edeb42492fedd0215d301d7a136.1ab14n19a900950205b44d033472de49
Please paste the Refresh Token in the instance configuration and save it for future use.
service-desk-plus-link-request-modify
Link or Unlink multiple commands
Base Command
service-desk-plus-link-request-modify
Input
| Argument Name | Description | Required |
|---|---|---|
| request_id | The id of the request for which the links should be modified | Required |
| action | Link / Unlink this request with the given requests | Required |
| linked_requests_id | The IDs of the requests that should be linked to the given request. Multiple IDs can be passed, separated by a comma | Required |
| comment | The comment that should be added when linking requests (optional). | Optional |
Context Output
There is no context output for this command.
Command Example
!service-desk-plus-link-request-modify action=Link request_id=123640000000284007 linked_requests_id=123640000000288001
Context Example
{}
Human Readable Output
Request successfully linked
service-desk-plus-request-close
Closes the specified request.
Base Command
service-desk-plus-request-close
Input
| Argument Name | Description | Required |
|---|---|---|
| closure_comments | The comments that should be added when closing the request | Optional |
| closure_code | The closure code to add to the request. Can be “Cancelled”, “Failed”, “Postponed”, “Rejected”, “Success” or “Unable to Reproduce” | Optional |
| requester_ack_comments | The requester comments that should be added to the request | Optional |
| requester_ack_resolution | Boolean. | Optional |
| request_id | The id of the request that should be closed | Required |
Context Output
There is no context output for this command.
Command Example
!service-desk-plus-request-close request_id=123640000000288001
Context Example
{}
Human Readable Output
Successfully closed request 123640000000288001
service-desk-plus-request-notes-list
Get a request specific note, or list all of its notes.
Base Command
service-desk-plus-request-notes-list
Input
| Argument Name | Description | Required |
|---|---|---|
| request_id | The ID of the request to retrieve its notes. | Required |
| request_note_id | The ID of the note to retrieve it’s data. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ServiceDeskPlus.Request.Note.mark_first_response | Boolean | Mark first response or not. |
| ServiceDeskPlus.Request.Note.created_time.display_value | Date | Created time display value. |
| ServiceDeskPlus.Request.Note.created_time.value | Date | Created time value. |
| ServiceDeskPlus.Request.Note.add_to_linked_requests | Boolean | Add to linked requests or not. |
| ServiceDeskPlus.Request.Note.request.display_id | String | Request display id. |
| ServiceDeskPlus.Request.Note.request.subject | String | Request subject. |
| ServiceDeskPlus.Request.Note.request.id | String | Request id. |
| ServiceDeskPlus.Request.Note.notify_technician | Boolean | Notify technician or not. |
| ServiceDeskPlus.Request.Note.show_to_requester | Boolean | Show to requester or not. |
| ServiceDeskPlus.Request.Note.description | String | Note description. |
| ServiceDeskPlus.Request.Note.id | Date | Note id. |
| ServiceDeskPlus.Request.Note.created_by.email_id | String | Email id of the creator. |
| ServiceDeskPlus.Request.Note.created_by.is_technician | Boolean | Whether the creator is technician or not. |
| ServiceDeskPlus.Request.Note.created_by.sms_mail | String | Creator sms mail. |
| ServiceDeskPlus.Request.Note.created_by.phone | String | Creator phone. |
| ServiceDeskPlus.Request.Note.created_by.name | String | Creator name. |
| ServiceDeskPlus.Request.Note.created_by.mobile | String | Creator mobile. |
| ServiceDeskPlus.Request.Note.created_by.id | Date | Creator id. |
| ServiceDeskPlus.Request.Note.created_by.photo_url | String | Creator photo url. |
| ServiceDeskPlus.Request.Note.created_by.is_vip_user | Boolean | Whether creator is vip user or not. |
service-desk-plus-request-notes-add
add a request note.
Base Command
service-desk-plus-request-notes-add
Input
| Argument Name | Description | Required |
|---|---|---|
| request_id | The ID of the request to retrieve it’s notes. | Required |
| description | The note description. | Optional |
| mark_first_response | whether to mark first response or not. Possible values are: true, false. Default is false. | Optional |
| add_to_linked_requests | whether to add to linked requests or not. Possible values are: true, false. Default is false. | Optional |
| notify_technician | whether to notify technician. Possible values are: true, false. Default is false. | Optional |
| show_to_requester | whether to show to requester. Possible values are: true, false. Default is false. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ServiceDeskPlus.Request.Note.mark_first_response | Boolean | mark first response or not. |
| ServiceDeskPlus.Request.Note.created_time.display_value | Date | created time display value. |
| ServiceDeskPlus.Request.Note.created_time.value | Date | created time value. |
| ServiceDeskPlus.Request.Note.add_to_linked_requests | Boolean | add to linked requests or not. |
| ServiceDeskPlus.Request.Note.request.display_id | String | request display id. |
| ServiceDeskPlus.Request.Note.request.subject | String | request subject. |
| ServiceDeskPlus.Request.Note.request.id | Date | request id. |
| ServiceDeskPlus.Request.Note.notify_technician | Boolean | notify technician or not. |
| ServiceDeskPlus.Request.Note.show_to_requester | Boolean | show to requester or not. |
| ServiceDeskPlus.Request.Note.description | String | note description. |
| ServiceDeskPlus.Request.Note.id | Date | note id. |
| ServiceDeskPlus.Request.Note.created_by.email_id | String | email id of the creator. |
| ServiceDeskPlus.Request.Note.created_by.is_technician | Boolean | whether the creator is technician or not. |
| ServiceDeskPlus.Request.Note.created_by.sms_mail | String | creator sms mail. |
| ServiceDeskPlus.Request.Note.created_by.phone | String | creator phone. |
| ServiceDeskPlus.Request.Note.created_by.name | String | creator name. |
| ServiceDeskPlus.Request.Note.created_by.mobile | String | creator mobile. |
| ServiceDeskPlus.Request.Note.created_by.id | Date | creator id. |
| ServiceDeskPlus.Request.Note.created_by.photo_url | String | creator photo url. |
| ServiceDeskPlus.Request.Note.created_by.is_vip_user | Boolean | whether creator is vip user or not. |
service-desk-plus-request-notes-delete
delete a request note.
Base Command
service-desk-plus-request-notes-delete
Input
| Argument Name | Description | Required |
|---|---|---|
| request_id | The ID of the request to delete it’s notes. | Required |
| request_note_id | The ID of the note to delete. | Required |
Context Output
There is no context output for this command.
service-desk-plus-request-notes-update
update a request note.
Base Command
service-desk-plus-request-notes-update
Input
| Argument Name | Description | Required |
|---|---|---|
| request_note_id | The ID of the note to retrieve it’s data. | Required |
| request_id | The ID of the request to retrieve it’s notes. | Required |
| description | The note description. | Required |
| mark_first_response | whether to mark first response or not. Possible values are: true, false. Default is false. | Optional |
| add_to_linked_requests | whether to add to linked requests or not. Possible values are: true, false. Default is false. | Optional |
| notify_technician | whether to notify technician. Possible values are: true, false. Default is false. | Optional |
| show_to_requester | whether to show to requester. Possible values are: true, false. Default is false. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ServiceDeskPlus.Request.Note.mark_first_response | Boolean | mark first response or not. |
| ServiceDeskPlus.Request.Note.created_time.display_value | Date | created time display value. |
| ServiceDeskPlus.Request.Note.created_time.value | Date | created time value. |
| ServiceDeskPlus.Request.Note.add_to_linked_requests | Boolean | add to linked requests or not. |
| ServiceDeskPlus.Request.Note.request.display_id | String | request display id. |
| ServiceDeskPlus.Request.Note.request.subject | String | request subject. |
| ServiceDeskPlus.Request.Note.request.id | Date | request id. |
| ServiceDeskPlus.Request.Note.notify_technician | Boolean | notify technician or not. |
| ServiceDeskPlus.Request.Note.show_to_requester | Boolean | show to requester or not. |
| ServiceDeskPlus.Request.Note.description | String | note description. |
| ServiceDeskPlus.Request.Note.id | Date | note id. |
| ServiceDeskPlus.Request.Note.created_by.email_id | String | email id of the creator. |
| ServiceDeskPlus.Request.Note.created_by.is_technician | Boolean | whether the creator is technician or not. |
| ServiceDeskPlus.Request.Note.created_by.sms_mail | String | creator sms mail. |
| ServiceDeskPlus.Request.Note.created_by.phone | String | creator phone. |
| ServiceDeskPlus.Request.Note.created_by.name | String | creator name. |
| ServiceDeskPlus.Request.Note.created_by.mobile | String | creator mobile. |
| ServiceDeskPlus.Request.Note.created_by.id | Date | creator id. |
| ServiceDeskPlus.Request.Note.created_by.photo_url | String | creator photo url. |
| ServiceDeskPlus.Request.Note.created_by.is_vip_user | Boolean | whether creator is vip user or not. |
Configuration parameters
server_url— Data Center Location: Select the domain location that is applicable to your application (required)technician_key— Technician Keycredentials_technician_key—server_url_on_premise— On-Premise Server URLclient_id— Client IDclient_secret— Client Secretcredentials_client— Client IDrefresh_token— Refresh Tokencredentials_refresh_token—insecure— Trust any certificate (not secure)proxy— Use system proxy settingsisFetch— Fetch incidentsincidentType— Incident typeincidentFetchInterval— Incidents Fetch Intervalfetch_status— Incidents status. Supports multi-select.fetch_time— First fetch timefetch_limit— Fetch limit - maximum number of incidents per fetchfetch_filter— Fetch query - see (?). Overrides the status filter, if given.
Commands (16)
-
service-desk-plus-generate-refresh-tokenThis function generates the refresh token that should be used in the instance configurations.
-
service-desk-plus-link-request-modifyLinks or unlinks multiple commands.
-
service-desk-plus-linked-request-listGets a list with all the linked requests under the specified request.
-
service-desk-plus-request-assignAssigns the specified request to a technician and/or group.
-
service-desk-plus-request-closeCloses the specified request.
-
service-desk-plus-request-createCreates new requests.
-
service-desk-plus-request-deleteDeletes the request with the specified ID. To delete multiple IDs, pass a comma-separated list (string) of the IDs of the requests.
-
service-desk-plus-request-notes-addAdds a request note.
-
service-desk-plus-request-notes-deleteDeletes a request note.
-
service-desk-plus-request-notes-listGet a request specific note, or list all of its notes.
-
service-desk-plus-request-notes-updateUpdates a request note.
-
service-desk-plus-request-pickupAllows a technician to pickup the specified with the given request ID on the technician's name.
-
service-desk-plus-request-resolution-addAdds a resolution to the specified request.
-
service-desk-plus-request-resolutions-listGets the resolution of the specified request.
-
service-desk-plus-request-updateUpdates the specified request.
-
service-desk-plus-requests-listView the details of requests. If no arguments are passed, details of all requests are returned.
import demistomock as demisto from CommonServerPython import * from CommonServerUserPython import * """ IMPORTS """ import ast from _collections import defaultdict from typing import Any import urllib3 # Disable insecure warnings urllib3.disable_warnings() """ CONSTANTS """ API_VERSION = "/api/v3/" OAUTH_URL = { "United States": "https://accounts.zoho.com/oauth/v2/token", "Europe": "https://accounts.zoho.eu/oauth/v2/token", "India": "https://accounts.zoho.in/oauth/v2/token", "China": "https://accounts.zoho.cn/oauth/v2/token", "Australia": "https://accounts.zoho.au/oauth/v2/token", } REQUEST_FIELDS = [ "subject", "description", "request_type", "impact_details", "impact", "status", "mode", "level", "urgency", "priority", "service_category", "requester", "assets", "site", "group", "technician", "category", "subcategory", "item", "email_ids_to_notify", "is_fcr", "resources", "udf_fields", "update_reason", ] FIELDS_WITH_NAME = [ "request_type", "impact", "status", "mode", "level", "urgency", "priority", "service_category", "requester", "site", "group", "technician", "category", "subcategory", "item", ] FIELDS_TO_IGNORE = ["has_draft", "cancel_flag_comments"] HUMAN_READABLE_FIELDS = ["CreatedTime", "Id", "Requester", "Technician", "Status", "Subject"] FIELDS_WITH_TIME = [ "created_time", "deleted_on", "due_by_time", "first_response_due_by_time", "responded_time", "resolved_time", "completed_time", "assigned_time", "last_updated_time", "submitted_on", ] SERVER_URL = { "United States": "https://sdpondemand.manageengine.com", "Europe": "https://sdpondemand.manageengine.eu", "India": "https://sdpondemand.manageengine.in", "China": "https://servicedeskplus.cn", "Australia": "https://servicedeskplus.net.au", } class Client(BaseClient): """ Client will implement the service API, and should not contain any Demisto logic. Should only do requests and return data. """ def __init__( self, url: str, outh_url: str, use_ssl: bool, use_proxy: bool, client_id: str = None, client_secret: str = None, refresh_token: str = None, technician_key: str = None, fetch_time: str = "7 days", fetch_status: list = None, fetch_limit: int = 50, fetch_filter: str = "", on_premise: bool = False, ): if fetch_status is None: fetch_status = [] self.client_id = client_id self.outh_url = outh_url self.client_secret = client_secret self.refresh_token = refresh_token self.technician_key = technician_key self.fetch_time = fetch_time self.fetch_status = fetch_status self.fetch_limit = fetch_limit self.fetch_filter = fetch_filter self.on_premise = on_premise if on_premise: super().__init__( url, verify=use_ssl, proxy=use_proxy, headers={"Accept": "application/v3+json", "TECHNICIAN_KEY": technician_key} ) else: super().__init__(url, verify=use_ssl, proxy=use_proxy, headers={"Accept": "application/v3+json"}) if self.refresh_token: self.get_access_token() # Add a valid access token to the headers def get_access_token(self): """ Gets an access token that was previously created if it is still valid, else, generates a new access token from the client id, client secret and refresh token """ previous_token = demisto.getIntegrationContext() # Check if there is an existing valid access token if previous_token.get("access_token") and previous_token.get("expiry_time") > date_to_timestamp(datetime.now()): access_token = previous_token.get("access_token") else: params = { "refresh_token": self.refresh_token, "grant_type": "refresh_token", "client_id": self.client_id, "client_secret": self.client_secret, } try: res = self.http_request("POST", url_suffix="", full_url=self.outh_url, params=params) if "error" in res: return_error( f"Error occurred while creating an access token. Please check the Client ID, Client Secret " f"and Refresh Token.\n{res}" ) if res.get("access_token"): expiry_time = date_to_timestamp(datetime.now(), date_format="%Y-%m-%dT%H:%M:%S") expiry_time += res.get("expires_in") * 1000 - 10 new_token = {"access_token": res.get("access_token"), "expiry_time": expiry_time} demisto.setIntegrationContext(new_token) access_token = res.get("access_token") except Exception as e: return_error( f"Error occurred while creating an access token. Please check the Client ID, Client Secret" f" and Refresh Token.\n\n{e.args[0]}" ) self._headers.update({"Authorization": "Bearer " + access_token}) def http_request(self, method, url_suffix, full_url=None, params=None): ok_codes = (200, 201, 401) # includes responses that are ok (200) and error responses that should be # handled by the client and not in the BaseClient try: res = self._http_request( method, url_suffix, full_url=full_url, resp_type="response", ok_codes=ok_codes, params=params, ) if res.status_code in [200, 201]: try: return res.json() except ValueError as exception: raise DemistoException(f"Failed to parse json object from response: {res.content}", exception) if res.status_code in [401]: if not self.on_premise and demisto.getIntegrationContext().get("expiry_time", 0) <= date_to_timestamp( datetime.now() ): self.get_access_token() return self.http_request(method, url_suffix, full_url=full_url, params=params) try: err_msg = f"Unauthorized request - check domain location and the given credentials \n{res.json()!s}" except ValueError: err_msg = f"Unauthorized request - check domain location and the given credentials -\n{res!s}" raise DemistoException(err_msg) except Exception as e: if "SSL Certificate Verification Failed" in e.args[0]: return_error( "SSL Certificate Verification Failed - try selecting 'Trust any certificate' " "checkbox in the integration configuration." ) raise DemistoException(e.args[0]) def get_requests(self, request_id: str = None, params: dict = None): if request_id: return self.http_request(method="GET", url_suffix=f"requests/{request_id}") else: return self.http_request(method="GET", url_suffix="requests", params=params) def create_output(request: dict) -> dict: """ Creates the output for the context and human readable from the response of an http_request Args: request: A single request dict returned from the http_request Returns: A dictionary containing all valid fields in the request """ output = {} for field in request: value = request.get(field, None) if value not in [None, {}, []] and field not in FIELDS_TO_IGNORE: output[string_to_context_key(field)] = value if field in FIELDS_WITH_TIME: output[string_to_context_key(field)] = timestamp_to_datestring(request.get(field, {}).get("value")) if output.get("Status"): output["Status"] = request.get("status", {}).get("name") return output def args_to_query(args: dict) -> dict: """ Converts the given demisto.args into the format required for the http request Args: args: The arguments for the current command. Returns: A dictionary containing all valid valid query field that were passed in the args, converted into the format required for the http_request. """ request_fields: dict[str, Any] = {} for field in REQUEST_FIELDS: value = args.get(field, None) if value: if field == "udf_fields": request_fields[field] = f"{create_udf_field(value)}" elif field not in FIELDS_WITH_NAME or (value[0] == "{" and value[-1] == "}"): # if the second condition # holds the user entered an object as the field value and not only the name of the field. For more # information please refer to the `service-desk-plus-request-create` command in the README. request_fields[field] = value else: request_fields[field] = {"name": value} return {"request": request_fields} def create_udf_field(udf_input: str): """ Converts the given string with udf keys and values to a valid dictionary for the query. Args: udf_input: the string representing the udf values as given by the user. Returns: A dictionary where every key is the udf_field key and the value given by the user. """ if not udf_input: return {} try: if udf_input[0] == "{" and udf_input[-1] == "}": # check if the user entered a dict as the value return ast.literal_eval(udf_input) fields = udf_input.split(",") udf_dict = {} for field in fields: if field: field_key_value = field.split(":") if field_key_value[0] and field_key_value[1]: udf_dict[field_key_value[0]] = field_key_value[1] else: raise Exception("Invalid input") return udf_dict except Exception: raise Exception( "Illegal udf fields format. Input format should be a string of key and value separated by : " "Multiple key;value pairs can be given, separated with a comma" ) def create_modify_linked_input_data(linked_requests_id: list, comment: str) -> dict: """ Returning the input_data dictionary that should be used to link/unlink the requests were passed. Args: linked_requests_id: the requests that should be linked/unlinked with/from the given base request comment: the comment that should be added when linking requests (optional) Returns: A dictionary containing the input_data parameter that should be used for linking/un-linking the requests. """ all_linked_requests = [] for request_id in linked_requests_id: linked_request: dict[str, Any] = {"linked_request": {"id": request_id}} if comment: linked_request["comments"] = comment all_linked_requests.append(linked_request) return {"link_requests": all_linked_requests} def create_human_readable(output: dict) -> dict: """ Converts the output of a command to a human readable output Args: output: the output that should be converted to the human readable representation Returns: dict: the dictionary that represents the human readable output """ hr = {} for field in HUMAN_READABLE_FIELDS: if output.get(field): hr[field] = output.get(field) if field in ["Technician", "Requester"]: hr[field] = output.get(field, {}).get("name") return hr def resolution_human_readable(output: dict) -> dict: """ Creates the human readable dictionary from the output of the resolution of the request Args: output: The resolution output that was created for the called request Returns: A dictionary containing all the valid fields in the resolution output """ hr = {} for key in output: if key == "SubmittedBy": hr["SubmittedBy"] = output.get("SubmittedBy", {}).get("name", "") else: hr[key] = output.get(key, "") return hr def note_human_readable(res: dict | list) -> str: if isinstance(res, dict): prefix = f"Note of request {res.get('request', {}).get('id')}:" res = [res] else: prefix = f"Notes of request {res[0].get('request', {}).get('id')}:" hr = [ { "Note ID": note.get("id"), "Description": note.get("description"), "Created time": note.get("created_time", {}).get("display_value"), } for note in res ] return tableToMarkdown(prefix, t=hr) def create_requests_list_info(start_index, row_count, search_fields, filter_by): """ Returning the list_info dictionary that should be used to filter the requests that are being returned. Args: start_index: the index of the first request that should be returned row_count: the number of requests that should be returned search_fields: search for specific fields in the requests filter_by: the filter by which to filter the returned requests Returns: A dictionary containing the list_info parameter that should be used for filtering the requests. """ list_info = {} if start_index is not None: list_info["start_index"] = start_index if row_count is not None: list_info["row_count"] = row_count if search_fields: list_info["search_fields"] = search_fields if filter_by: list_info["filter_by"] = filter_by list_info["sort_field"] = "created_time" list_info["sort_order"] = "asc" return {"list_info": list_info} def create_fetch_list_info(time_from: str, time_to: str, status: list, fetch_filter: str, fetch_limit: int) -> dict: """ Returning the list_info dictionary that should be used to filter the requests that are being fetched The requests that will be returned when using this list_info are all requests created between 'time_from' and 'time_to' (inclusive) and are with the given status, in ascending order of creation time. Args: time_from: the time from which requests should be fetched time_to: the time until which requests should be fetched status: the status of the requests that should be fetched fetch_filter: a string representing all the field according to which the results that are being fetched should be filtered. Multiple fields, separated with a comma, can be used to filter. Every field should be in the following format: 'field-name condition field-value' where condition is the condition that this field should satisfy, for example 'is', 'is not', 'greater than' etc. fetch_limit: the maximal number of requests that should be returned. Returns: A dictionary containing the list_info parameter that should be used for filtering the requests. """ list_info = {} try: search_criteria = [{"field": "created_time", "values": [f"{time_from}", f"{time_to}"], "condition": "between"}] if fetch_filter: filters = ast.literal_eval(fetch_filter) if isinstance(filters, dict): query: dict[str, Any] = { "field": filters.get("field"), "condition": filters.get("condition"), "values": filters.get("values", "").split(","), "logical_operator": filters.get("logical_operator", "AND"), } if filters.get("logical_operator") == "OR": raise Exception('Only "AND" is allowed as a logical_operator') search_criteria.append(query) else: for filter in filters: query = { "field": filter.get("field"), "condition": filter.get("condition"), "values": filter.get("values", "").split(","), "logical_operator": filter.get("logical_operator", "AND"), } if filter.get("logical_operator") == "OR": raise Exception('Only "AND" is allowed as a logical_operator') search_criteria.append(query) else: if status: query = {"field": "status.name", "values": status, "condition": "is", "logical_operator": "AND"} search_criteria.append(query) list_info = { "search_criteria": search_criteria, "sort_field": "created_time", "sort_order": "asc", "row_count": fetch_limit, } except Exception as e: return_error( f"Invalid input format for fetch query. Please see detailed information (?) for valid fetch query " f"format.\n{e.args[0]}" ) return {"list_info": list_info} # Command functions: def list_requests_command(client: Client, args: dict): """ Get the details of requests. The returned requests can be filtered by a single request id or by input_data param. Args: client: Client object with request. args: Usually demisto.args() Returns: Demisto Outputs. """ request_id = args.get("request_id", None) start_index = args.get("start_index", None) row_count = args.get("page_size", None) search_fields = args.get("search_fields", None) filter_by = args.get("filter_by", None) list_info = create_requests_list_info(start_index, row_count, search_fields, filter_by) params = {"input_data": f"{list_info}"} result = client.get_requests(request_id, params) output = [] hr = [] context: dict = defaultdict(list) if request_id: requests = [result.get("request", [])] else: requests = result.get("requests", []) for request in requests: request_output = create_output(request) output.append(request_output) hr.append(create_human_readable(request_output)) context["ServiceDeskPlus(val.ID===obj.ID)"] = {"Request": output} markdown = tableToMarkdown("Requests", t=hr) return markdown, context, result def delete_request_command(client: Client, args: dict) -> tuple[str, dict, Any]: """ Delete the request(s) with the given request_id Args: client: Client object with request. args: Usually demisto.args() Returns: Demisto Outputs. """ request_id = args.get("request_id", "") requests_list = request_id.split(",") result = {} for request in requests_list: result = client.http_request("DELETE", url_suffix=f"requests/{request}") hr = f"### Successfully deleted request(s) {requests_list}" return hr, {}, result def create_request_command(client: Client, args: dict) -> tuple[str, dict, Any]: """ Create a new request with the given args Args: client: Client object with request. args: Usually demisto.args() Returns: Demisto Outputs. """ query = args_to_query(args) params = {"input_data": f"{query}"} result = client.http_request("POST", url_suffix="requests", params=params) request = result.get("request", None) output = {} context: dict = defaultdict(list) if request: output = create_output(request) hr = create_human_readable(output) markdown = tableToMarkdown("Service Desk Plus request was successfully created", t=hr) context["ServiceDeskPlus(val.ID===obj.ID)"] = {"Request": output} return markdown, context, result def update_request_command(client: Client, args: dict) -> tuple[str, dict, Any]: """ Updates an existing request with the given args Args: client: Client object with request. args: Usually demisto.args() Returns: Demisto Outputs. """ query = args_to_query(args) params = {"input_data": f"{query}"} request_id = args.get("request_id") result = client.http_request("PUT", url_suffix=f"requests/{request_id}", params=params) request = result.get("request", None) output = {} context: dict = defaultdict(list) if request: output = create_output(request) hr = create_human_readable(output) markdown = tableToMarkdown("Service Desk Plus request was successfully updated", t=hr) context["ServiceDeskPlus(val.ID===obj.ID)"] = {"Request": output} return markdown, context, result def assign_request_command(client: Client, args: dict) -> tuple[str, dict, Any]: """ Assigns the given request to the given technician/group Args: client: Client object with request. args: Usually demisto.args() Returns: Demisto Outputs. """ query = args_to_query(args) params = {"input_data": f"{query}"} request_id = args.get("request_id") result = client.http_request("PUT", url_suffix=f"requests/{request_id}/_assign", params=params) markdown = f"### Service Desk Plus request {request_id} was successfully assigned" return markdown, {}, result def pickup_request_command(client: Client, args: dict) -> tuple[str, dict, Any]: """ Picks up the given request to the current technician Args: client: Client object with request. args: Usually demisto.args() Returns: Demisto Outputs. """ request_id = args.get("request_id") result = client.http_request("PUT", url_suffix=f"requests/{request_id}/pickup") markdown = f"### Service Desk Plus request {request_id} was successfully picked up" return markdown, {}, result def linked_request_command(client: Client, args: dict) -> tuple[str, dict, Any]: """ Lists all the requests that are linked to the given request. Args: client: Client object with request. args: Usually demisto.args() Returns: Demisto Outputs. """ request_id = args.get("request_id") result = client.http_request("GET", url_suffix=f"requests/{request_id}/link_requests") linked_requests = result.get("link_requests", []) context: dict = defaultdict(list) output = [] for request in linked_requests: request_output = create_output(request) output.append(request_output) markdown = tableToMarkdown(f"Linked requests to request {request_id}", t=output, removeNull=True) context["ServiceDeskPlus.Request(val.ID===obj.ID)"] = {"LinkRequests": output} return markdown, context, result def modify_linked_request_command(client: Client, args: dict) -> tuple[str, dict, Any]: """ Links/Un-links the given request with all the other requests that where passed as arguments. Args: client: Client object with request. args: Usually demisto.args() Returns: Demisto Outputs. """ request_id = args.get("request_id") action = args.get("action") linked_requests_id = args.get("linked_requests_id", "").split(",") comment = args.get("comment", "") input_data = create_modify_linked_input_data(linked_requests_id, comment) params = {"input_data": f"{input_data}"} if action == "Link": result = client.http_request("POST", url_suffix=f"requests/{request_id}/link_requests", params=params) else: result = client.http_request("DELETE", url_suffix=f"requests/{request_id}/link_requests", params=params) markdown = f"## {result.get('response_status', {}).get('messages')[0].get('message')}" return markdown, {}, result def add_resolution_command(client: Client, args: dict) -> tuple[str, dict, Any]: """ Adds the resolution to the given request Args: client: Client object with request. args: Usually demisto.args() Returns: Demisto Outputs. """ request_id = args.get("request_id") resolution_content = args.get("resolution_content") add_to_linked_requests = args.get("add_to_linked_requests") if args.get("add_to_linked_requests") else "false" query = {"resolution": {"content": resolution_content, "add_to_linked_requests": add_to_linked_requests}} params = {"input_data": f"{query}"} result = client.http_request("POST", url_suffix=f"requests/{request_id}/resolutions", params=params) if add_to_linked_requests == "true": markdown = f"### Resolution was successfully added to {request_id} and the linked requests" else: markdown = f"### Resolution was successfully added to {request_id}" return markdown, {}, result def get_resolutions_list_command(client: Client, args: dict) -> tuple[str, dict, Any]: """ Gets the resolution of the given request Args: client: Client object with request. args: Usually demisto.args() Returns: Demisto Outputs. """ request_id = args.get("request_id") result = client.http_request("GET", url_suffix=f"requests/{request_id}/resolutions") context: dict = defaultdict(list) output = create_output(result.get("resolution", {})) hr = {} if output: context["ServiceDeskPlus.Request(val.ID===obj.ID)"] = {"Resolution": output} hr = resolution_human_readable(output) markdown = tableToMarkdown(f"Resolution of request {request_id}", t=hr) return markdown, context, result def close_request_command(client: Client, args: dict) -> tuple[str, dict, Any]: """ Close the request with the given request_id with comments and resolution defined by the user. Args: client: Client object with request. args: Usually demisto.args() Returns: Demisto Outputs. """ request_id = args.get("request_id") closure_info: dict[str, Any] = { "requester_ack_resolution": args.get("requester_ack_resolution", "false"), "requester_ack_comments": args.get("requester_ack_comments", ""), "closure_comments": args.get("closure_comments", ""), } if args.get("closure_code"): closure_info["closure_code"] = {"name": args.get("closure_code")} input_data = {"request": {"closure_info": closure_info}} params = {"input_data": f"{input_data}"} result = client.http_request("PUT", url_suffix=f"requests/{request_id}/_close", params=params) hr = f"### Successfully closed request {request_id}" return hr, {}, result def get_request_notes_list_command(client: Client, args: dict) -> CommandResults: request_id = args.get("request_id") request_note_id = args.get("request_note_id") if request_note_id: raw_result = client.http_request("GET", url_suffix=f"requests/{request_id}/notes/{request_note_id}") res = raw_result.get("request_note") else: raw_result = client.http_request("GET", url_suffix=f"requests/{request_id}/notes") res = raw_result.get("notes") return CommandResults( outputs_prefix="ServiceDeskPlus.Request.Note", outputs_key_field="id", outputs=res, readable_output=note_human_readable(res), raw_response=raw_result, ) def add_request_note_command(client: Client, args: dict) -> CommandResults: request_id = args.get("request_id") description = args.get("description") data = { "request_note": { "mark_first_response": argToBoolean(args.get("mark_first_response")) or False, "add_to_linked_requests": argToBoolean(args.get("add_to_linked_requests")) or False, "notify_technician": argToBoolean(args.get("notify_technician")) or False, "show_to_requester": argToBoolean(args.get("show_to_requester")) or False, "description": description, } } raw_result = client.http_request(method="POST", url_suffix=f"requests/{request_id}/notes", params={"input_data": f"{data}"}) res = raw_result.get("request_note") return CommandResults( outputs_prefix="ServiceDeskPlus.Request.Note", outputs_key_field="id", outputs=res, readable_output=note_human_readable(res), raw_response=raw_result, ) def delete_request_note_command(client: Client, args: dict) -> CommandResults: request_id = args.get("request_id") request_note_id = args.get("request_note_id") raw_result = client.http_request("DELETE", url_suffix=f"requests/{request_id}/notes/{request_note_id}") res = raw_result.get("request_note") return CommandResults( outputs_prefix="ServiceDeskPlus.Request.Note", outputs_key_field="id", outputs=res, readable_output=f"The request note {request_note_id} has been successfully deleted from the request {request_id}.", raw_response=raw_result, ) def update_request_note_command(client: Client, args: dict) -> CommandResults: request_id = args.get("request_id") request_note_id = args.get("request_note_id") description = args.get("description") data = { "request_note": { "mark_first_response": argToBoolean(args.get("mark_first_response")) or False, "add_to_linked_requests": argToBoolean(args.get("add_to_linked_requests")) or False, "notify_technician": argToBoolean(args.get("notify_technician")) or False, "show_to_requester": argToBoolean(args.get("show_to_requester")) or False, "description": description, } } raw_result = client.http_request( method="PUT", url_suffix=f"requests/{request_id}/notes/{request_note_id}", params={"input_data": f"{data}"} ) res = raw_result.get("request_note") return CommandResults( outputs_prefix="ServiceDeskPlus.Request.Note", outputs_key_field="id", outputs=res, readable_output=note_human_readable(res), raw_response=raw_result, ) def fetch_incidents(client: Client, test_command: bool = False) -> list: date_format = "%Y-%m-%dT%H:%M:%S" last_run = {} if not test_command: last_run = demisto.getLastRun() if not last_run: # if first time running try: new_last_run = {"time": date_to_timestamp(parse_date_range(client.fetch_time, date_format=date_format, utc=False)[0])} except Exception as e: return_error(f"Invalid fetch time range.\n{e.args[0]}") else: new_last_run = last_run demisto_incidents: list = [] time_from = new_last_run.get("time") time_to = date_to_timestamp(datetime.now(), date_format=date_format) list_info = create_fetch_list_info( str(time_from), str(time_to), client.fetch_status, client.fetch_filter, client.fetch_limit + 1 ) params = {"input_data": f"{list_info}"} # Get incidents from Service Desk Plus demisto.info(f"Fetching ServiceDeskPlus incidents. with the query params: {params!s}") incidents = client.get_requests(params=params).get("requests", []) if incidents: count = 0 last_run_id = last_run.get("id", "0") last_incident_id = last_run.get("id", "0") cur_time = new_last_run.get("time", 0) incident_creation_time = new_last_run.get("time", 0) for incident in incidents: if count >= client.fetch_limit: break # Prevent fetching twice the same incident - the last incident that was fetched in the last run, will be the # first incident in the returned incidents from the API call this time. if incident.get("id") == last_run_id: continue incident_creation_time = int(incident.get("created_time", {}).get("value")) if incident_creation_time >= cur_time: demisto_incidents.append( { "name": f'{incident.get("subject")} - {incident.get("id")}', "occurred": timestamp_to_datestring(incident_creation_time), "rawJSON": json.dumps(incident), } ) count += 1 last_incident_id = incident.get("id") if demisto_incidents: new_last_run.update({"time": incident_creation_time, "id": last_incident_id}) if not demisto_incidents: new_last_run.update({"time": time_to}) if not test_command: demisto.setLastRun(new_last_run) return demisto_incidents def test_module(client: Client): """ Returning 'ok' indicates that the integration works like it is supposed to. Connection to the service is successful. If 'Fetches incidents' is checked in the instance configurations, this function checks that the entered parameters are valid. Args: client: Service Desk Plus client Returns: 'ok' if test passed, anything else will fail the test. """ if not client.on_premise and not client.refresh_token: return_error("Please enter a refresh token (see detailed instruction (?) for more information)") try: client.http_request("GET", "requests") params: dict = demisto.params() if params.get("isFetch"): fetch_incidents(client, test_command=True) return "ok" except Exception as e: raise e def generate_refresh_token(client: Client, args: dict) -> tuple[str, dict, Any]: """ Creates for the user the refresh token for the app, given the code the user got when defining the scopes of the app. Args: client: Service Desk Plus client args: demisto.args() containing the code Returns: If the code is valid and the Refresh Token was generated successfully, the function displays the refresh token for the user in the war room. """ if client.on_premise: return_error("The command 'service-desk-plus-generate-refresh-token' can not be executed on on-premise.") code = args.get("code") params = { "code": code, "grant_type": "authorization_code", "client_id": client.client_id, "client_secret": client.client_secret, } res = client.http_request("POST", url_suffix="", full_url=client.outh_url, params=params) if res.get("refresh_token"): hr = ( f'### Refresh Token: {res.get("refresh_token")}\n Please paste the Refresh Token in the instance ' f'configuration and save it for future use.' ) elif res.get("error"): hr = f'### Error: {res.get("error")}' else: hr = res return hr, {}, None def main(): params = demisto.params() region = params.get("server_url") technician_key = params.get("credentials_technician_key", {}).get("password") or params.get("technician_key") client_id = params.get("credentials_client", {}).get("identifier") or params.get("client_id") client_secret = params.get("credentials_client", {}).get("password") or params.get("client_secret") refresh_token = params.get("credentials_refresh_token", {}).get("password") or params.get("refresh_token") if region == "On-Premise": client = Client( url=params.get("server_url_on_premise") + API_VERSION, outh_url=OAUTH_URL["United States"], use_ssl=not params.get("insecure", False), use_proxy=params.get("proxy", False), technician_key=technician_key, fetch_time=params.get("first_fetch") if params.get("first_fetch") else "7 days", fetch_status=params.get("fetch_status"), fetch_limit=int(params.get("max_fetch")) if params.get("max_fetch") else 50, fetch_filter=params.get("fetch_filter") if params.get("fetch_filter") else "", on_premise=True, ) else: server_url = SERVER_URL[region] client = Client( url=server_url + API_VERSION, outh_url=OAUTH_URL[region], use_ssl=not params.get("insecure", False), use_proxy=params.get("proxy", False), client_id=client_id, client_secret=client_secret, refresh_token=refresh_token, fetch_time=params.get("fetch_time") if params.get("fetch_time") else "7 days", fetch_status=params.get("fetch_status"), fetch_limit=int(params.get("fetch_limit")) if params.get("fetch_limit") else 50, fetch_filter=params.get("fetch_filter") if params.get("fetch_filter") else "", ) commands = { "service-desk-plus-generate-refresh-token": generate_refresh_token, "service-desk-plus-requests-list": list_requests_command, "service-desk-plus-request-delete": delete_request_command, "service-desk-plus-request-create": create_request_command, "service-desk-plus-request-update": update_request_command, "service-desk-plus-request-assign": assign_request_command, "service-desk-plus-request-pickup": pickup_request_command, "service-desk-plus-request-close": close_request_command, "service-desk-plus-linked-request-list": linked_request_command, "service-desk-plus-link-request-modify": modify_linked_request_command, "service-desk-plus-request-resolution-add": add_resolution_command, "service-desk-plus-request-resolutions-list": get_resolutions_list_command, } notes_commands = { "service-desk-plus-request-notes-list": get_request_notes_list_command, "service-desk-plus-request-notes-add": add_request_note_command, "service-desk-plus-request-notes-delete": delete_request_note_command, "service-desk-plus-request-notes-update": update_request_note_command, } command = demisto.command() LOG(f"Command being called is {command}") try: if command == "test-module": demisto.results(test_module(client)) elif command == "fetch-incidents": incidents = fetch_incidents(client) demisto.incidents(incidents) elif command in commands: return_outputs(*commands[command](client, demisto.args())) elif command in notes_commands: return_results(notes_commands[command](client, demisto.args())) else: return_error("Command not found.") except Exception as e: return_error(f"Failed to execute {command} command. Error: {e}") if __name__ in ("__main__", "__builtin__", "builtins"): main()