OTRS

Service management suite that comprises ticketing, workflow automation, and notification.

Case Management · OTRS Service Management XSOAR Pack

Details

IDOTRS
ProviderEasyVista
CategoryCase Management
From Version5.0.0
Docker Imagedemisto/pyotrs:1.0.0.10133006
Supported ModulesAgentix 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

  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for OTRS.
  3. 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
  1. 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 settings
  • isFetch — Fetch incidents
  • incidentType — Incident type
  • fetch_queue — Queues to fetch tickets from ("Any" fetches from all queues. CSV supported, e.g., Misc,Raw)
  • fetch_priority — Fetch tickets in priority
  • fetch_time — First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days, 3 months, 1 year)
  • look_back — Days to look back when fetching
  • use_legacy_sessions — Use legacy sessions
  • incidentFetchInterval — Incidents Fetch Interval
  • tag — Entry Tag To OTRS
  • tag_from_otrs — Entry Tag From OTRS
  • mirror_direction — Incident Mirroring Direction
  • webservice_config_ticket — Web Service Config for Tickets

Commands (5)

  • otrs-close-ticket

    Close an OTRS ticket.

  • otrs-create-ticket

    Create a new ticket in OTRS.

  • otrs-get-ticket

    Retrieves 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-ticket

    Search for an OTRS ticket using search filters.

  • otrs-update-ticket

    Update 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()