OTRS
Service management suite that comprises ticketing, workflow automation, and notification.
Case Management · OTRS Service Management XSOAR Pack
Details
| ID | OTRS |
|---|---|
| Provider | EasyVista |
| Category | Case Management |
| From Version | 5.0.0 |
| Docker Image | demisto/pyotrs:1.0.0.10133006 |
| Supported Modules | Agentix XSIAM |
README
Service management suite that comprises ticketing, workflow automation, and notification.
This integration was integrated and tested with OTRS versions 5, 6, and 7.
Prerequisite
Before configuring OTRS on Cortex XSOAR, you need to enable the webservices in your OTRS instance. It is recommended to use the provided YAML webservice configuration template, which includes the Route: /TicketList endpoint required for PyOTRS but which is not included in the default OTRS webservice setup. If you use a different file than the template, make sure to name your file GenericTicketConnectorREST.yml.
This integration uses an optional JSON routing table corresponding to the aforementioned webservice configuration template which defaults to this JSON value in PyOTRS. The parameter webservice_config_ticket may be used to supply a different JSON configuration if your instance implements other endpoints or expects other outputs (e.g. changing the Result of SessionGet to AccessToken in newer versions).
Configure OTRS on Cortex XSOAR
- Navigate to Settings > Integrations > Servers & Services.
- Search for OTRS.
- Click Add instance to create and configure a new integration instance.
| Parameter | Description | Required |
|---|---|---|
| server | OTRS Server URL (for example http://example.com ) | True |
| credentials | OTRS Credentials | True |
| unsecure | Trust any certificate (not secure) | False |
| proxy | Use system proxy settings | False |
| isFetch | Fetch incidents | False |
| incidentType | Incident type | False |
| fetch_queue | Queues to fetch tickets from ("Any" fetches from all queues. CSV supported, for example Misc, Raw) | False |
| fetch_priority | Fetch tickets in priority | False |
| fetch_time | First fetch timestamp (formatted as <number> <time unit>, for example 12 hours, 7 days, 3 months, 1 year) | False |
| look_back | Days to look back when fetching | False |
| webservice_config_ticket | JSON representation of the webservice configuration for tickets as described in the Prerequisite section | 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.
otrs-get-ticket
Retrieves details for an OTRS ticket by ticket ID or ticket number. At least one input argument is required for the integration to run.
Base Command
otrs-get-ticket
Input
| Argument Name | Description | Required |
|---|---|---|
| ticket_id | Ticket ID of the ticket to get details for. If not spcecified, the ticket_number argument is required. | Optional |
| ticket_number | Ticket Number of the ticket to get details for. If not specified, the ticket_id argument is required. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| OTRS.Ticket.ID | string | Ticket ID |
| OTRS.Ticket.Number | string | Ticket number |
| OTRS.Ticket.Created | date | Ticket creation date |
| OTRS.Ticket.CustomerUser | string | Customer user related to the ticket |
| OTRS.Ticket.Owner | string | Ticket owner |
| OTRS.Ticket.Priority | string | Ticket priority |
| OTRS.Ticket.Queue | string | Queue the ticket is in |
| OTRS.Ticket.State | string | Ticket state |
| OTRS.Ticket.Title | string | Ticket title |
| OTRS.Ticket.Type | string | Ticket type |
| OTRS.Ticket.DynamicField | string | Ticket dynamic fields |
| OTRS.Ticket.Article.Subject | string | Ticket article subject |
| OTRS.Ticket.Article.Body | string | Ticket article body |
| OTRS.Ticket.Article.CreatedTime | date | Ticket article creation time |
| OTRS.Ticket.Article.ContentType | string | Ticket article content type |
| OTRS.Ticket.Article.From | string | Ticket article sender |
| OTRS.Ticket.Article.ID | string | Ticket article ID |
| OTRS.Ticket.Article.Attachment.Name | string | Ticket article attachment file name |
| OTRS.Ticket.Article.Attachment.Size | number | Ticket article attachment file size |
| OTRS.Ticket.Article.Attachment.ContentType | string | Ticket article attachment file content type |
| OTRS.Ticket.Lock | string | Is the ticket locked or unlocked |
| File.Size | number | Size of the file attachment |
| File.SHA1 | string | SHA-1 of the file attachment |
| File.SHA256 | string | SHA-256 of the file attachment |
| File.Name | string | Attachment file name |
| File.SSDeep | string | Attachment file SSDeep |
| File.EntryID | string | Attachment file entry ID |
| File.Info | string | Attachment file information |
| File.Type | string | Attachment file type |
| File.MD5 | string | Attachment file MD5 |
| File.Extension | string | Attachment file extension |
Command Example
!otrs-get-ticket ticket_id="7023"
Context Example
{
"OTRS": {
"Ticket": {
"Age": "0 h 09 m",
"Article": [
{
"Body": "Testing",
"ContentType": "text/plain; charset=utf8",
"CreateTime": "2020-04-26 11:05:07",
"From": "\"Jens Bothe\" <jens.bothe@otrs.com\>",
"ID": "11187",
"Subject": "TestArticle"
},
{
"Body": "ClosingBody",
"ContentType": "text/plain; charset=utf8",
"CreateTime": "2020-04-26 11:05:12",
"From": "SIEM Webservice",
"ID": "11188",
"Subject": "ClosingSubject"
}
],
"Created": "2020-04-26 11:05:07",
"CustomerID": "jb",
"DynamicField": {
"Firstname": "Jens",
"Gender": "male"
},
"ID": "7023",
"Lock": "unlock",
"Number": "2020042610000031",
"Owner": "siem",
"Priority": "1 very low",
"Queue": "Inbox::SIEM",
"State": "open",
"Title": "UpdatedTitle",
"Type": "Incident"
}
}
}
Human Readable Output
OTRS Ticket 7023
| ID | Number | Age | Title | State | Lock | Queue | Owner | CustomerID | Priority | Type | Created | DynamicField |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 7023 | 2020042610000031 | 0 h 09 m | UpdatedTitle | open | unlock | Inbox::SIEM | siem | jb | 1 very low | Incident | 2020-04-26 11:05:07 | Firstname: Jens Gender: male |
Articles
| ID | From | Subject | Body | CreateTime | ContentType |
|---|---|---|---|---|---|
| 11187 | “Jens Bothe” <jens.bothe@otrs.com> | TestArticle | Testing | 2020-04-26 11:05:07 | text/plain; charset=utf8 |
| 11188 | SIEM Webservice | ClosingSubject | ClosingBody | 2020-04-26 11:05:12 | text/plain; charset=utf8 |
otrs-search-ticket
Search for an OTRS ticket using search filters
Base Command
otrs-search-ticket
Input
| Argument Name | Description | Required |
|---|---|---|
| state | Ticket states to filter for in CSV format (for example New, Open) | Optional |
| created_before | Filter for a ticket created before this date. (formatted as <number> <time unit>, for example 1 day, 30 minutes, 2 weeks, 6 months, 1 year) | Optional |
| created_after | Filter for a ticket created after this date. (formatted as <number> <time unit>, for example 1 day, 30 minutes, 2 weeks, 6 months, 1 year) | Optional |
| title | Ticket Title to filter for | Optional |
| queue | Ticket Queues to filter for in CSV format (for example Raw,Misc) | Optional |
| priority | Ticket priority to filter for in CSV format (for example 4High,5VeryHigh). For the use of a custom priority, you can specify a custom value outside the predefined set. | Optional |
| type | Ticket type to filter for | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| OTRS.Ticket.ID | string | Ticket ID |
| OTRS.Ticket.Number | string | Ticket number |
| OTRS.Ticket.Created | date | Ticket creation date |
| OTRS.Ticket.CustomerUser | string | Customer user related to ticket |
| OTRS.Ticket.Owner | string | Ticket owner |
| OTRS.Ticket.Priority | string | Ticket priority |
| OTRS.Ticket.Queue | string | Queue the ticket is in |
| OTRS.Ticket.State | string | Ticket state |
| OTRS.Ticket.Title | string | Ticket title |
| OTRS.Ticket.Type | string | Ticket type |
Command Example
!otrs-search-ticket state="PendingReminder" title="7023"
Context Example
{}
Human Readable Output
No results found
otrs-create-ticket
Create a new ticket in OTRS
Base Command
otrs-create-ticket
Input
| Argument Name | Description | Required |
|---|---|---|
| title | Title to assign to the new ticket | Required |
| queue | Queue to place the new ticket in | Required |
| state | State to assign to the new ticket. For the use of a custom state, you can specify a custom value outside the predefined set. | Required |
| priority | Priority to assign to the new ticket. For the use of a custom priority, you can specify a custom value outside the predefined set. | Required |
| customer_user | Customer user related to the new ticket | Required |
| article_subject | Article subject to apply to the new ticket | Required |
| article_body | Text to add to the article body of the new ticket | Required |
| article_content_type | Content Type of the article body. Can be either ‘text/plain; charset=utf8’ or ‘text/html; charset=utf8’. Defaults to ‘text/plain; charset=utf8’. | Optional |
| type | Ticket type to assign to the new ticket | Optional |
| dynamic_fields | Dynamic fields to apply to the new ticket in the format: field1=value1,field2=value2. For example: ProcessManagementProcessID=1,ProcessManagementActivityStatus=2 | Optional |
| attachment | File entry ID of the file to add as an attachment to the new ticket in CSV format. For example: 123@20,124@21 | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| OTRS.Ticket.Article.Subject | string | Ticket article subject |
| OTRS.Ticket.Article.Body | string | Ticket article body |
| OTRS.Ticket.Article.ContentType | string | Content Type of the article body. |
| OTRS.Ticket.ID | string | Ticket ID |
| OTRS.Ticket.Number | string | Ticket number |
| OTRS.Ticket.Created | date | Ticket creation date |
| OTRS.Ticket.Priority | string | Ticket priority |
| OTRS.Ticket.Queue | string | Queue that the ticket is in |
| OTRS.Ticket.State | string | Ticket state |
| OTRS.Ticket.Title | string | Ticket title |
| OTRS.Ticket.Type | string | Ticket type |
| OTRS.Ticket.CustomerUser | string | Customer user related to ticket |
| OTRS.Ticket.DynamicField | string | Ticket dynamic fields |
Command Example
!otrs-create-ticket title="TestTicket" queue="Inbox::SIEM" state="New" priority="2Low" customer_user="jb" article_subject="TestArticle" article_body="Testing" type="Unclassified"
Context Example
{
"OTRS": {
"Ticket": {
"Article": {
"Body": "Testing",
"Subject": "TestArticle"
},
"CustomerUser": "jb",
"DynamicField": [],
"ID": "7024",
"Number": "2020042610000049",
"Priority": "2 low",
"Queue": "Inbox::SIEM",
"State": "new",
"Title": "TestTicket",
"Type": "Unclassified"
}
}
}
Human Readable Output
Created ticket 7024 successfully
otrs-update-ticket
Update an OTRS ticket
Base Command
otrs-update-ticket
Input
| Argument Name | Description | Required |
|---|---|---|
| ticket_id | Ticket ID of the ticket to update | Required |
| title | Ticket title of the ticket to update | Optional |
| state | Ticket state of the ticket to update. For the use of a custom state, you can specify a custom value outside the predefined set. | Optional |
| priority | Priority of the ticket to update. For the use of a custom priority, you can specify a custom value outside the predefined set. | Optional |
| article_subject | Article subject of the ticket to update | Optional |
| article_body | Article body of the ticket to update | Optional |
| article_content_type | Content Type of the article body. Can be either ‘text/plain; charset=utf8’ or ‘text/html; charset=utf8’. Defaults to ‘text/plain; charset=utf8’. | Optional |
| queue | Queue that the ticket to update is in | Optional |
| type | Ticket type of the ticket to update | Optional |
| dynamic_fields | Dynamic fields to apply to the updated ticket, in the format: field1=value1,field2=value2. For example: ProcessManagementProcessID=1,ProcessManagementActivityStatus=2 | Optional |
| attachment | File entry ID of the file to add as an attachment to the updated ticket in CSV format. For example: 123@20,124@21 | Optional |
| lock | Wether to change the lock state of the ticket. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| OTRS.Ticket.Article.Subject | string | Ticket article subject |
| OTRS.Ticket.Article.Body | string | Ticket article body |
| OTRS.Ticket.Article.ContentType | string | Content Type of the article body. |
| OTRS.Ticket.ID | string | Ticket ID |
| OTRS.Ticket.Created | date | Ticket creation date |
| OTRS.Ticket.Priority | string | Ticket priority |
| OTRS.Ticket.Queue | string | Queue that the ticket is in |
| OTRS.Ticket.State | string | Ticket state |
| OTRS.Ticket.Title | string | Ticket title |
| OTRS.Ticket.Type | string | Ticket type |
Command Example
!otrs-update-ticket ticket_id="7023" title="UpdatedTitle" state="Open" priority="1VeryLow" type="Incident"
Context Example
{
"OTRS": {
"Ticket": {
"ID": "7023",
"Priority": "1 very low",
"State": "open",
"Title": "UpdatedTitle",
"Type": "Incident"
}
}
}
Human Readable Output
Updated ticket 7023 successfully
otrs-close-ticket
Close an OTRS ticket
Base Command
otrs-close-ticket
Input
| Argument Name | Description | Required |
|---|---|---|
| ticket_id | Ticket ID of the ticket to close | Required |
| article_subject | Article subject of the ticket to close | Required |
| article_body | Article body of the ticket to close | Required |
| article_content_type | Content Type of the article body. Can be either ‘text/plain; charset=utf8’ or ‘text/html; charset=utf8’. Defaults to ‘text/plain; charset=utf8’. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| OTRS.Ticket.ID | string | Ticket ID |
| OTRS.Ticket.State | string | Ticket state |
| OTRS.Ticket.Article.Subject | string | Ticket article subject |
| OTRS.Ticket.Article.Body | string | Ticket article body |
| OTRS.Ticket.Article.ContentType | string | Content Type of the article body. |
Command Example
!otrs-close-ticket ticket_id="7023" article_subject="ClosingSubject" article_body="ClosingBody"
Context Example
{
"OTRS": {
"Ticket": {
"Article": {
"Body": "ClosingBody",
"Subject": "ClosingSubject"
},
"ID": "7023",
"State": "closed successful"
}
}
}
Human Readable Output
Closed ticket 7023 successfully
Configuration parameters
server— OTRS Server URL (e.g. http://example.com) (required)credentials— OTRS Credentials (required)unsecure— Trust any certificate (not secure)proxy— Use system proxy settingsisFetch— Fetch incidentsincidentType— Incident typefetch_queue— Queues to fetch tickets from ("Any" fetches from all queues. CSV supported, e.g., Misc,Raw)fetch_priority— Fetch tickets in priorityfetch_time— First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days, 3 months, 1 year)look_back— Days to look back when fetchinguse_legacy_sessions— Use legacy sessionsincidentFetchInterval— Incidents Fetch Intervaltag— Entry Tag To OTRStag_from_otrs— Entry Tag From OTRSmirror_direction— Incident Mirroring Directionwebservice_config_ticket— Web Service Config for Tickets
Commands (5)
-
otrs-close-ticketClose an OTRS ticket.
-
otrs-create-ticketCreate a new ticket in OTRS.
-
otrs-get-ticketRetrieves details for an OTRS ticket by ticket ID or ticket number. The arguments are both optional, but at least one is required for this integration to run.
-
otrs-search-ticketSearch for an OTRS ticket using search filters.
-
otrs-update-ticketUpdate an OTRS ticket.
import demistomock as demisto # noqa: F401 from CommonServerPython import * # noqa: F401 """ IMPORTS """ from collections.abc import Callable from typing import Any from urllib.parse import unquote import urllib3 from pyotrs import Article, Attachment, Client, DynamicField, Ticket # disable insecure warnings urllib3.disable_warnings() """ GLOBAL VARS """ MIRROR_DIRECTION = {"None": None, "Incoming": "In", "Outgoing": "Out", "Incoming And Outgoing": "Both"} DEFAULT_ARTICLE_CONTENT_TYPE = "text/plain; charset=utf8" """ HELPER FUNCTIONS """ def ticket_to_incident(ticket: dict[str, Any]): attachments_list = [] articles = ticket.get("Article") if articles: for article in articles: attachments = article.get("Attachment") if attachments: for attachment in attachments: file_name = attachment["Filename"] attachment_file = fileResult(file_name, base64.b64decode(attachment["Content"])) attachments_list.append({"path": attachment_file["FileID"], "name": file_name}) incident = { "attachment": attachments_list, "rawJSON": unquote(json.dumps(ticket)), "name": "OTRS ticket {}".format(ticket["TicketID"]), } return incident def translate_state(state: str): state_dict = { "ClosedSuccessful": "closed successful", "ClosedUnsuccessful": "closed unsuccessful", "Open": "open", "PendingReminder": "pending reminder", "New": "new", } return state_dict.get(state, state) def translate_priority(priority: str): priority_dict = { "1VeryLow": "1 very low", "2Low": "2 low", "3Normal": "3 normal", "4High": "4 high", "5VeryHigh": "5 very high", } return priority_dict.get(priority, priority) def calculate_age(seconds: int): """ Convert seconds to time period string e.g. 6000 -> 1 h 40 m """ m, s = divmod(seconds, 60) h, m = divmod(m, 60) return f"{h} h {m} m" def demisto_entry_to_otrs_attachment(entry_list: list[Any]): """ Convert Demisto file entry to OTRS attachment object """ attachments = [] for file in entry_list: file_path = demisto.getFilePath(file) with open(file_path["path"], "rb") as file_content: encoded_file = base64.b64encode(file_content.read()).decode("utf-8") # Encoding file content in base64, # as required by OTRS and then decode it as mentioned in https://gitlab.com/rhab/PyOTRS/-/issues/18 # Getting file type from context context_files = demisto.get(demisto.context(), "File") if isinstance(context_files, dict): # If there's only one file in context, we will get a dict and we convert it to list context_files = [context_files] content_type = None for context_file in context_files: # Looking for file entry in context if context_file["EntryID"] == file: content_type = context_file["Info"] break if content_type is None: raise Exception("Could not find file in context") otrs_attachment = Attachment.create_basic( # Creating OTRS attachment object Filename=file_path["name"], Content=encoded_file, ContentType=content_type ) attachments.append(otrs_attachment) return attachments def get_mirroring(): """ Get tickets mirroring. """ params = demisto.params() return { "mirror_direction": MIRROR_DIRECTION.get(params.get("mirror_direction")), "mirror_tags": [ params.get("tag"), # tag to otrs ], "mirror_instance": demisto.integrationInstance(), } """ CLASS """ class OTRSClient: def __init__( self, base_url: str, username: str, password: str, https_verify: bool, use_legacy_sessions: bool, webservice_config_ticket: dict | None = None, ): self.client = Client( base_url, username, password, https_verify=https_verify, use_legacy_sessions=use_legacy_sessions, webservice_config_ticket=webservice_config_ticket, ) cache = demisto.getIntegrationContext() # OTRS creates new session for each request, to avoid that behavior - # save the sessionId in integration context to use it multiple times if cache.get("SessionID"): self.client.session_id_store.write(cache["SessionID"]) else: self.update_session() def get_ticket(self, ticket_id: str): args = {"ticket_id": ticket_id, "articles": True, "attachments": True, "dynamic_fields": True} response = self.execute_otrs_method(self.client.ticket_get_by_id, args) raw_ticket = response.to_dct()["Ticket"] return raw_ticket def get_ticket_by_number(self, ticket_number: str): args = {"ticket_number": ticket_number, "articles": True, "attachments": True, "dynamic_fields": True} response = self.execute_otrs_method(self.client.ticket_get_by_number, args) raw_ticket = response.to_dct().get("Ticket") return raw_ticket def search_ticket( self, states: list[Any] | None = None, created_before: datetime | None = None, created_after: datetime | None = None, title: str | None = None, queue: list[Any] | None = None, priority: list[Any] | None = None, ticket_type: str | None = None, article_create_time_newer_minutes: int | None = None, pattern: str | None = None, ): args = { "States": states, "TicketCreateTimeOlderDate": created_before, "TicketCreateTimeNewerDate": created_after, "Title": title, "Queues": queue, "Priorities": priority, "Types": ticket_type, "ArticleCreateTimeNewerMinutes": article_create_time_newer_minutes, } if pattern: args["FullTextIndex"] = "1" args["ContentSearch"] = "OR" args["MIMEBase_Body"] = f"%{pattern}%" args["MIMEBase_Subject"] = f"%{pattern}%" return self.execute_otrs_method(self.client.ticket_search, args) def create_ticket(self, new_ticket: Ticket, article: Article | None, df: list[Any] | None, attachments: list[Any] | None): args = {"ticket": new_ticket, "article": article, "dynamic_fields": df, "attachments": attachments} return self.execute_otrs_method(self.client.ticket_create, args) def update_ticket( self, ticket_id: str, title: str | None = None, queue: str | None = None, state: str | None = None, priority: str | None = None, article: Article | None = None, ticket_type: str | None = None, df: list[Any] | None = None, attachments: list[Any] | None = None, owner: str | None = None, customer_user: str | None = None, lock: str | None = None, ): args = { "ticket_id": ticket_id, "Title": title, "Queue": queue, "State": state, "Priority": priority, "article": article, "dynamic_fields": df, "attachments": attachments, "Owner": owner, "CustomerUser": customer_user, "Type": ticket_type, "Lock": lock, } return self.execute_otrs_method(self.client.ticket_update, args) def update_session(self): self.client.session_create() sessionID = self.client.session_id_store.value demisto.setIntegrationContext({"SessionID": sessionID}) self.client.session_id_store.write(sessionID) def execute_otrs_method(self, method: Callable[..., Any], args: dict[str, Any]): try: response = method(**args) except Exception: self.update_session() response = method(**args) return response """ FUNCTIONS """ def get_ticket_command(client: Client, args: dict[str, str]): ticket_id = args.get("ticket_id") ticket_number = args.get("ticket_number") if ticket_id and ticket_number is None: ticket = client.get_ticket(ticket_id) elif ticket_id is None and ticket_number: ticket = client.get_ticket_by_number(ticket_number) else: raise Exception("Exactly one ticket identifier is required in order to retrieve a ticket, ticket_id or ticket_number!") output = { "ID": str(ticket["TicketID"]), "Number": ticket["TicketNumber"], "Created": ticket["Created"], "CustomerID": ticket["CustomerUserID"], "Owner": ticket["Owner"], "Priority": ticket["Priority"], "Queue": ticket["Queue"], "State": ticket["State"], "Title": ticket["Title"], "Type": ticket["Type"], "Lock": ticket["Lock"], "Age": calculate_age(ticket["Age"]), } df = ticket.get("DynamicField") if df: output["DynamicField"] = {} for field in df: value = field["Value"] if value: name = field["Name"] output["DynamicField"][name] = value title = "OTRS Ticket " + str(ticket["TicketID"]) headers = [ "ID", "Number", "Age", "Title", "State", "Lock", "Queue", "Owner", "CustomerID", "Priority", "Type", "Created", "DynamicField", ] human_readable = tableToMarkdown(title, output, headers=headers, removeNull=True) attachments_list = [] articles = ticket.get("Article") if articles: articles_list = [] human_readable_articles = [] for article in articles: # Get article details current_article = { "ID": str(article["ArticleID"]), "Subject": article.get("Subject"), "Body": article.get("Body"), "CreateTime": article.get("CreateTime"), "From": article.get("From"), "ContentType": article.get("ContentType"), } currect_human_readable_article = dict(current_article) # Get attachments attachments = article.get("Attachment") if attachments: attachments_output = [] attachments_str = "" for attachment in attachments: file_name = attachment["Filename"] file_size = attachment["FilesizeRaw"] content_type = attachment["ContentType"] current_attachment = {"Name": file_name, "Size": file_size, "ContentType": content_type} attachments_str += f"Name: {file_name}, Size: {file_size}, ContentType: {content_type}" attachments_str += "\n\n" attachments_list.append(fileResult(file_name, base64.b64decode(attachment["Content"]))) attachments_output.append(current_attachment) currect_human_readable_article["Attachment"] = attachments_str current_article["Attachment"] = attachments_output human_readable_articles.append(currect_human_readable_article) articles_list.append(current_article) human_readable += tableToMarkdown( "Articles", human_readable_articles, headers=["ID", "From", "Subject", "Body", "CreateTime", "ContentType", "Attachment"], removeNull=True, ) output["Article"] = articles_list return_results( CommandResults( outputs=output, outputs_prefix="OTRS.Ticket", outputs_key_field="ID", readable_output=human_readable, raw_response=ticket, ) ) return attachments_list def search_ticket_command(client: Client, args: dict[str, str]): states = args.get("state") if states: states = argToList(states) created_before = args.get("created_before") if created_before: created_before, _ = parse_date_range(created_before) created_after = args.get("created_after") if created_after: created_after, _ = parse_date_range(created_after) title = args.get("title") queue = args.get("queue") if queue: queue = argToList(queue) priority: list[Any] | None = None if args.get("priority"): priority_list: list[str] = argToList(args.get("priority")) priority = [translate_priority(p) for p in priority_list] ticket_type = args.get("type") pattern = args.get("pattern") tickets = client.search_ticket(states, created_before, created_after, title, queue, priority, ticket_type, pattern=pattern) if tickets: output = [] raw_output = [] for ticket_id in tickets: raw_ticket = client.get_ticket(ticket_id) ticket = { "ID": str(raw_ticket["TicketID"]), "Number": raw_ticket["TicketNumber"], "Created": raw_ticket["Created"], "Owner": raw_ticket["Owner"], "Priority": raw_ticket["Priority"], "Queue": raw_ticket["Queue"], "State": raw_ticket["State"], "Title": raw_ticket["Title"], "Type": raw_ticket["Type"], } output.append(ticket) raw_output.append(raw_ticket) title = "OTRS Search Results" headers = ["ID", "Number", "Title", "Type", "State", "Priority", "Queue", "Created", "Owner"] return CommandResults( outputs=output, outputs_prefix="OTRS.Ticket", outputs_key_field="ID", readable_output=tableToMarkdown(title, output, headers), raw_response=raw_output, ) else: return "No results found" def create_ticket_command(client: Client, args: dict[str, str]): title = args.get("title") queue = args.get("queue") state = translate_state(args["state"]) priority = translate_priority(args["priority"]) customer_user = args.get("customer_user") owner = args.get("owner") article_subject = args.get("article_subject") article_body = args.get("article_body") article_content_type = args.get("article_content_type", DEFAULT_ARTICLE_CONTENT_TYPE) ticket_type = args.get("type") dynamic_fields = args.get("dynamic_fields") attachment = args.get("attachment") df = [] df_output = [] if dynamic_fields: dynamic_fields_list = argToList(dynamic_fields) for field in dynamic_fields_list: splitted_field = field.split("=") current_field, current_value = splitted_field[0], splitted_field[1] df.append(DynamicField(current_field, current_value)) df_output.append({current_field: current_value}) attachments = [] if attachment: attachments_list = argToList(attachment) attachments = demisto_entry_to_otrs_attachment(attachments_list) new_ticket = Ticket( { "Title": title, "Queue": queue, "State": state, "Priority": priority, "CustomerUser": customer_user, "Type": ticket_type, "Owner": owner, } ) article = Article({"Subject": article_subject, "Body": article_body, "ContentType": article_content_type}) ticket = client.create_ticket(new_ticket, article, df, attachments) context = { "ID": str(ticket["TicketID"]), "Number": ticket["TicketNumber"], "CustomerUser": customer_user, "Priority": priority, "Queue": queue, "State": state, "Title": title, "Article": {"Subject": article_subject, "Body": article_body, "ContentType": article_content_type}, "Type": ticket_type, "DynamicField": df_output, } output = "Created ticket {} successfully".format(ticket["TicketID"]) return CommandResults( outputs=context, outputs_prefix="OTRS.Ticket", outputs_key_field="ID", readable_output=output, raw_response=context ) def update_ticket_command(client: Client, args: dict[str, str]): ticket_id = args.get("ticket_id") title = args.get("title") queue = args.get("queue") state = args.get("state") owner = args.get("owner", None) lock = args.get("lock", "").lower() if args.get("lock", "").lower() in ["lock", "unlock"] else None customer_user = args.get("customer_user") priority = args.get("priority") article_subject = args.get("article_subject") article_body = args.get("article_body") article_content_type = args.get("article_content_type", DEFAULT_ARTICLE_CONTENT_TYPE) ticket_type = args.get("type") dynamic_fields = args.get("dynamic_fields") attachment = args.get("attachment") if all( v is None for v in [ title, queue, state, priority, article_subject, article_body, ticket_type, dynamic_fields, attachment, owner, customer_user, ] ): raise Exception("No fields to update were given") if (article_subject and article_body is None) or (article_subject is None and article_body): raise Exception("Both article subject and body are required in order to add article") elif article_subject and article_body: article_obj = {"Subject": article_subject, "Body": article_body, "ContentType": article_content_type} article = Article(article_obj) else: article = None if state: state = translate_state(state) if priority: priority = translate_priority(priority) df = [] if dynamic_fields: dynamic_fields_list = argToList(dynamic_fields) for field in dynamic_fields_list: splitted_field = field.split("=") current_field, current_value = splitted_field[0], splitted_field[1] df.append(DynamicField(current_field, current_value)) attachments = [] if attachment: attachments_list = argToList(attachment) attachments = demisto_entry_to_otrs_attachment(attachments_list) ticket = client.update_ticket( ticket_id, title, queue, state, priority, article, ticket_type, df, attachments, owner=owner, customer_user=customer_user, lock=lock, ) context = { "ID": ticket["TicketID"], } if priority: context["Priority"] = priority if queue: context["Queue"] = queue if state: context["State"] = state if title: context["Title"] = title if article: context["Article"] = article.to_dct() if ticket_type: context["Type"] = ticket_type output = "Updated ticket {} successfully".format(ticket["TicketID"]) return CommandResults( outputs=context, outputs_prefix="OTRS.Ticket", outputs_key_field=["ID"], readable_output=output, raw_response=context ) def close_ticket_command(client: Client, args: dict[str, str]): ticket_id = args.get("ticket_id") article_subject = args.get("article_subject") article_body = args.get("article_body") article_content_type = args.get("article_content_type", DEFAULT_ARTICLE_CONTENT_TYPE) state = args.get("state", "closed successful") article_object = {"Subject": article_subject, "Body": article_body, "ContentType": article_content_type} article = Article(article_object) ticket = client.update_ticket(ticket_id, article=article, state=state) context = {"ID": ticket["TicketID"], "State": state, "Article": article_object} output = "Closed ticket {} successfully".format(ticket["TicketID"]) return CommandResults( outputs=context, outputs_prefix="OTRS.Ticket", outputs_key_field=["ID"], readable_output=output, raw_response=context ) def fetch_incidents(client: Client, fetch_queue: str, fetch_priority: str, fetch_time: str, look_back_days: int): last_run_obj = demisto.getLastRun() last_run_time = last_run_obj.get("time") is_first_fetch = last_run_time is None last_fetched_ids = last_run_obj.get("last_fetched_ids", []) if is_first_fetch: last_run_time, _ = parse_date_range(fetch_time) else: last_run_time = datetime.strptime(last_run_time, "%Y-%m-%d %H:%M:%S") + timedelta(seconds=1) # in case that a specific queue is provided - we also look back in the search to find incidents created before the last run # but moved to the queue after the fetch and the fetch missed them. # the looked_back_last_run will be used only for search, # while in demisto.setLastRun the original last_run will be saved if no incident fetched. looked_back_last_run = last_run_time queue = None if "Any" in fetch_queue else argToList(fetch_queue) if queue and not is_first_fetch: looked_back_last_run -= timedelta(days=look_back_days) demisto.debug(f'the base time will be used in search: {datetime.strftime(looked_back_last_run, "%Y-%m-%d %H:%M:%S")}') priority = None if fetch_priority: priority = [translate_priority(p) for p in fetch_priority] raw_tickets = client.search_ticket(created_after=looked_back_last_run, queue=queue, priority=priority) tickets = [ticket_id for ticket_id in raw_tickets if ticket_id not in last_fetched_ids] demisto.debug(f"filter out {len(raw_tickets) - len(tickets)} already fetched tickets") incidents = [] first_ticket = True last_created = "" for ticket_id in tickets: ticket = client.get_ticket(ticket_id) ticket.update(get_mirroring()) incident = ticket_to_incident(ticket) incidents.append(incident) if first_ticket: # First ticket fetched is the last created, so should set its creation time as last fetched ticket last_created = ticket["Created"] first_ticket = False demisto.incidents(incidents) if not last_created: last_created = datetime.strftime(last_run_time, "%Y-%m-%d %H:%M:%S") demisto.setLastRun({"time": last_created, "last_fetched_ids": raw_tickets}) def get_remote_data_command(client: Client, args: dict[str, str]): params = demisto.params() ticket_id = args.get("id") headers = ["ArticleID", "To", "Cc", "Subject", "CreateTime", "From", "ContentType", "Body"] demisto.debug(f"Getting update for remote {ticket_id}") if args.get("lastUpdate"): last_update = round(datetime.strptime(args["lastUpdate"].split(".")[0], "%Y-%m-%dT%H:%M:%S").timestamp()) else: last_update = 0 retry_count = 3 demisto.debug(f"last_update is {last_update}") while retry_count: ticket = client.get_ticket(ticket_id) if not ticket: demisto.debug(f"Ticket with id {ticket_id} was not found.") retry_count -= 1 else: break if ticket: ticket_last_update = ticket["UnlockTimeout"] entries = [] if last_update > ticket_last_update: demisto.debug(f"Nothing new in the ticket since {last_update}") ticket = {} else: demisto.debug(f"ticket is updated: {ticket}") # get latest comments and files articles = ticket.get("Article") if articles: for article in articles: # Get article details description = tableToMarkdown("OTRS Mirroring Update", article, headers=headers, removeNull=True) if article["IncomingTime"] > last_update: entries.append( { "Type": EntryType.NOTE, "Contents": description, "ContentsFormat": EntryFormat.MARKDOWN, "Tags": [params.get("tag_from_otrs", "FromOTRS")], # the list of tags to add to the entry "Note": False, # boolean, True for Note, False otherwise } ) if article.get("Attachment"): for attachment in article.get("Attachment"): file = fileResult(attachment["Filename"], base64.b64decode(attachment["Content"])) file["Tags"] = [params.get("tag_from_otrs")] entries.append(file) return GetRemoteDataResponse(ticket, entries) else: return None def update_remote_system_command(client: Client, args: dict[str, str]): parsed_args = UpdateRemoteSystemArgs(args) demisto.debug(f"Sending incident with remote ID [{parsed_args.remote_incident_id}] to remote system\n") ticket_id: str = parsed_args.remote_incident_id if parsed_args.delta: demisto.debug(f"Got the following delta keys {list(parsed_args.delta.keys())}") if parsed_args.entries: for entry in parsed_args.entries: demisto.debug(f'Sending entry {entry.get("id")}') article_object = { "Subject": "Update from Cortex XSOAR", "Body": "File from XSOAR" if entry.get("file") else str(entry.get("contents", "")), } article = Article(article_object) if entry.get("file"): file_path = demisto.getFilePath(entry.get("id")) with open(file_path["path"], "rb") as file_content: encoded_file = base64.b64encode(file_content.read()).decode("utf-8") # Encoding file content in base64, # as required by OTRS and then decode it as mentioned in https://gitlab.com/rhab/PyOTRS/-/issues/18 otrs_attachment = Attachment.create_basic( # Creating OTRS attachment object Filename=file_path["name"], Content=encoded_file, ContentType=entry["fileMetadata"]["type"] ) client.update_ticket(ticket_id, article=article, attachments=[otrs_attachment]) else: client.update_ticket(ticket_id, article=article) # Close incident if relevant demisto.debug(f"Incident Status {parsed_args.inc_status}") if parsed_args.inc_status == 2: demisto.debug(f"Sending closure message to remote incident {ticket_id}") article_object = { "Subject": "Cortex XSOAR Alert closed - " + parsed_args.data.get("closeReason"), "Body": parsed_args.data.get("closeNotes"), } article = Article(article_object) client.update_ticket(ticket_id, article=article) return ticket_id def get_modified_remote_data_command(client: Client, args: dict[str, str]): demisto.debug("Performing get-modified-remote-data command for last 5 minutes.") raw_incidents = client.search_ticket(article_create_time_newer_minutes=5) demisto.debug(f"raw tickets: {raw_incidents}") return GetModifiedRemoteDataResponse(raw_incidents) def main(): params = demisto.params() base_url = params.get("server", "").strip("/") username = params.get("credentials", {}).get("identifier") password = params.get("credentials", {}).get("password") use_legacy_sessions = argToBoolean(params.get("use_legacy_sessions", False)) webservice_config_ticket = params.get("webservice_config_ticket", None) verify = not params.get("unsecure", False) fetch_queue = params.get("fetch_queue", "Any") fetch_priority = params.get("fetch_priority") fetch_time_default = "3 days" fetch_time = params.get("fetch_time", fetch_time_default) fetch_time = fetch_time if fetch_time and fetch_time.strip() else fetch_time_default look_back_days = int(params.get("look_back", 1)) handle_proxy(params.get("proxy")) if webservice_config_ticket and isinstance(webservice_config_ticket, str): webservice_config_ticket = safe_load_json(webservice_config_ticket) otrs_client = OTRSClient( base_url, username, password, https_verify=verify, use_legacy_sessions=use_legacy_sessions, webservice_config_ticket=webservice_config_ticket, ) args = demisto.args() demisto.info(f"command is {demisto.command()}") commands = { "otrs-get-ticket": get_ticket_command, "otrs-search-ticket": search_ticket_command, "otrs-create-ticket": create_ticket_command, "otrs-update-ticket": update_ticket_command, "otrs-close-ticket": close_ticket_command, "get-remote-data": get_remote_data_command, "update-remote-system": update_remote_system_command, "get-modified-remote-data": get_modified_remote_data_command, } try: if demisto.command() == "test-module": # Testing connectivity and credentials return_results("ok") elif demisto.command() == "fetch-incidents": fetch_incidents(otrs_client, fetch_queue, fetch_priority, fetch_time, look_back_days) elif demisto.command() in commands: return_results(commands[demisto.command()](otrs_client, args)) else: raise NotImplementedError(f"Command not implemented: {demisto.command()}") except Exception as e: demisto.info(str(e)) return_error(str(e)) if __name__ in ("__main__", "__builtin__", "builtins"): main()