jira-v2 Deprecated

Deprecated. Use the Atlassian Jira v3 integration instead

Case Management · Atlassian Jira

Details

IDjira-v2
ProviderAtlassian
CategoryCase Management
From Version5.0.0
Docker Imagedemisto/oauthlib:1.0.0.72372
Supported ModulesAgentix Cortex Cloud Cloud Runtime Security Cloud Posture Security XSIAM EDR

README

Use the Atlassian Jira v2 integration to manage Jira issues and create Cortex XSOAR incidents from Jira projects.

This integration was integrated and tested with: Jira Cloud, Jira v8.19.1.
For more information about JQL syntax, go to https://www.atlassian.com/software/jira/guides/expand-jira/jql.

Use Cases


  1. Create, edit, delete, and query Jira issues.
  2. Get or add to the comments of an issue.
  3. Add a link or upload an attachment to an issue.

Configure jira-v2 on Cortex XSOAR

  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for jira-v2.
  3. Authentiction: As of June 2019, basic authentication using passwords for Jira Cloud is no longer supported. Use an API token or OAuth 1.0 instead. As of this writing, Jira Data Center (unlike Jira Cloud) still supports basic authentication.
  4. Click Add instance to create and configure a new integration instance.
    • ______ Basic Authentication ______

      To use basic authentication, follow this tutorial to get the API token. Authorizing the use of basic authentication requires:

      • Username
      • Password (Deprecated)
      • API token
    • ________ OAuth 1.0 ______

      To use OAuth1.0 follow this tutorial to get the Access token. Authorizing the use of OAuth1.0 requires:

      • ConsumerKey
      • AccessToken
      • PrivateKey
    Parameter Description Required
    Jira URL, for example: https://demisto.atlassian.net/   True
    Username (API or Basic Authentication)   False
    Password (Deprecated - Use API token)   False
    API token   False
    Consumer key (OAuth 1.0) Will be ignored if other required fields are not provided (for example both the Access Token and the Private Key). False
    Access token Used for both OAuth 1.0 and Personal Access Token authentication methods. False
    Private key (PKCS8 format)   False
    Query (in JQL) for fetching incidents   False
    Issue index to start fetching incidents from   False
    Trust any certificate (not secure)   False
    Use system proxy settings   False
    Fetch incidents   False
    Mirror incoming incidents   False
    Incident type   False
    Use created field to fetch incidents   False
    Mirror outgoing incidents   False
    File entry tag Choose the tag to add to an entry to mirror it as an attachment in Jira. False
    Comment entry tag Choose the tag to add to an entry to mirror it as a comment in Jira. False
    Fetch comments Fetch comments for Jira ticket. False
    Fetch attachments Fetch attachments for Jira ticket. False
  5. Click Test to validate the URLs, token, and connection.

Fetched Incidents Data


When you enable incidents fetching, Cortex XSOAR fetches the first batch of Jira issues from the 10 minutes prior to when the integration was added. After the first batch of fetched issues, Cortex XSOAR fetches new Jira issues as soon as they are generated in Jira. By default, 50 issues are fetched for each call. To fetch older Jira issues, use the query to fetch issues option.
If Fetch comments is enabled, The fetched incident will include the comments in the Jira issue.
If Fetch attachments is enabled, The fetched incident will include the attachments in the Jira issue.

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.

jira-issue-query


Queries Jira issues.

Base Command

jira-issue-query

Input

Argument Name Description Required
query The JQL query string. Required
startAt The index (integer) of the first issue to return (0-based). Optional
maxResults The maximum number of users to fetch when searching for a matching user (default is 50). The maximum allowed value is dictated by the Jira property ‘jira.search.views.default.max’. If you specify a value that is higher than this number, your search results will be truncated. Optional
headers Display the headers in human readable format. Optional
extraFields Specify the result fields. Optional

Context Output

Path Type Description
Ticket.Id Unknown The ID of the ticket.
Ticket.Key Unknown The key of the ticket.
Ticket.Assignee Unknown The user assigned to the ticket.
Ticket.Creator Unknown The user who created the ticket.
Ticket.Summary Unknown The summary of the ticket.
Ticket.Status Unknown The status of the ticket.
Ticket.Priority String The priority of the ticket.
Ticket.Description String The description of the ticket.
Ticket.Labels String The labels of the ticket.
Ticket.ProjectName String The ticket project name.
Ticket.DueDate Date The due date.
Ticket.Created Date The time the ticket was created.
Ticket.LastSeen Date The last time the ticket was viewed.
Ticket.LastUpdate Date The last time the ticket was updated.

Command Example

!jira-issue-query query="status=done"

Context Example
{
    "Ticket": [
        {
            "Status": "Done", 
            "Creator": "{creator}", 
            "Summary": "HelloBlocked11", 
            "Assignee": "null(null)", 
            "Key": "TES-25", 
            "Id": "12658"
        }, 
        {
            "Status": "Done", 
            "Creator": "{creator}", 
            "Summary": "Test2", 
            "Assignee": "null(null)", 
            "Key": "SOC-40", 
            "Id": "10986"
        }
    ]
}

Human Readable Output

jira-issue-query

assignee created creator description duedate id issueType key labels priority project reporter status summary ticket_link
null(null) 2019-05-04T02:45:09.909+0300 {creator} TypeofIssueIdList   12658 A task that needs to be done. TES-25   Medium test1 {creator} Done HelloBlocked11 https://demistodev.atlassian.net/rest/api/latest/issue/12658
null(null) 2019-01-27T15:59:03.134+0200 {creator}     10986 jira.translation.issuetype.bug.name.desc SOC-40   Medium SOC {creator} Done Test2 https://demistodev.atlassian.net/rest/api/latest/issue/10986

jira-get-issue


Fetches an issue from Jira.

Base Command

jira-get-issue

Input

Argument Name Description Required
issueId The ID of the issue. Required
headers Display the headers in human readable format. Optional
getAttachments If “true”, retrieves the issue’s attachments. Possible values are: true, false. Default is false. Optional
expandLinks If “true”, expands the issue’s links. Possible values are: true, false. Default is false. Optional

Context Output

Path Type Description
Ticket.Id Unknown The ID of the ticket.
Ticket.Key Unknown The key of ticket.
Ticket.Assignee Unknown The user assigned to the ticket.
Ticket.Creator Unknown The user who created the ticket.
Ticket.Summary Unknown The summary of the ticket.
Ticket.Status Unknown The status of the ticket.
File.Size Unknown The size of the file (Jira attachments are saved as files in Cortex XSOAR).
File.SHA256 Unknown The SHA256 hash of the file (Jira attachments are saved as files in Cortex XSOAR).
File.Name Unknown The name of the file (Jira attachments are saved as files in Cortex XSOAR).
File.SHA1 Unknown The SHA1 hash of the file (Jira attachments are saved as files in Cortex XSOAR).
Ticket.Priority String The priority of the ticket.
Ticket.ProjectName String The ticket project name.
Ticket.DueDate Date The due date.
Ticket.Created Date The time the ticket was created.
Ticket.LastSeen Date The last time the ticket was viewed.
Ticket.LastUpdate Date The last time the ticket was updated.
Command Example

!jira-get-issue issueId=15572 getAttachments=true

Context Example
{
    "Ticket": [
        {
            "Status": "To Do", 
            "Creator": "{creator}", 
            "Summary": "Test issue23", 
            "Assignee": "{assignee}", 
            "attachment": "", 
            "Key": "DEM-5415", 
            "Id": "15572"
        }
    ]
}

Human Readable Output

jira-get-issue

assignee attachment created creator description duedate id issueType key labels priority project reporter status summary ticket_link
null(null)   2020-01-19T12:34:13.784+0200 {creator} lala   15572 Request for Action DEM-5415   Medium demistodev {assignee} To Do Test issue23 https://demistodev.atlassian.net/rest/api/latest/issue/15572

jira-create-issue


Creates a new issue in Jira.

Base Command

jira-create-issue

Input

Argument Name Description Required
issueJson The issue object (in JSON format). Optional
summary The summary of the issue. Required
projectKey The project key with which to associate the issue. Optional
issueTypeName Select an issue type by name, for example: “Problem”. Optional
issueTypeId Select an issue type by its numeric ID. Optional
projectName The project name with which to associate the issue. Optional
description A description of the issue. Optional
labels A CSV list of labels. Optional
priority The priority of the issue, for example: High, Medium. Optional
dueDate The due date for the issue (in the format: 2018-03-11). Optional
assignee The name of the assignee. Relevant for Jira Server only. If you are using Jira Cloud, please provide the assignee_id argument instead. Optional
assignee_id The account ID of the assignee. Use the jira-get-id-by-attribute command to get the user’s account ID. Optional
reporter The name of the reporter. Optional
reporter_id The account ID of the reporter. Use the jira-get-id-by-attribute command to get the user’s account ID. Optional
parentIssueKey The parent issue key (if you’re creating a sub-task). Optional
parentIssueId The parent issue ID (if you’re creating a sub-task). Optional
environment A text field for describing the environment in which the issue occurred (for example - environment=”IE9 on Windows 7”). Optional
security The security level name of the issue, (for example - security=”Anyone”). Optional
components The components names of the issue, (for example - components=”component1,component2”). Optional

Context Output

Path Type Description
Ticket.Id Unknown The ID of the ticket.
Ticket.Key Unknown The key of the ticket.
Command Example

!jira-create-issue summary="test SOC issue26" issueTypeId=10008 projectKey=DEM issueJson=`{"fields":{"issuetype":{"name":"Request for Action"}}}`

Context Example
{
    "Ticket": [
        {
            "Id": "15576", 
            "Key": "DEM-5419"
        }
    ]
}

Human Readable Output

jira-create-issue

id key projectKey self
15576 DEM-5419 DEM https://demistodev.atlassian.net/rest/api/latest/issue/15576

jira-issue-upload-file


Uploads a file attachment to an issue.

Base Command

jira-issue-upload-file

Input

Argument Name Description Required
issueId The ID of the issue. Required
upload The entry ID to upload. Optional
attachmentName The attachment name to be displayed in Jira (overrides original file name). Optional

Context Output

There is no context output for this command.

Command Example

!jira-issue-upload-file issueId=15572 upload=19@75

Human Readable Output

jira-issue-upload-file

attachment_link attachment_name id issueId
https://demistodev.atlassian.net/rest/api/2/attachment/13456 jira_v2_yml.yml 13456 15572

jira-issue-add-comment


Adds a new comment to an existing Jira issue.

Base Command

jira-issue-add-comment

Input

Argument Name Description Required
issueId The ID of the issue. Required
comment The comment body. Required
visibility The roles that can view the comment, for example: Administrators. Optional

Context Output

There is no context output for this command.

Command Example

!jira-issue-add-comment issueId=15572 comment="test comment"

Human Readable Output

jira-issue-add-comment

comment id key ticket_link
test comment 13779 admin https://demistodev.atlassian.net/rest/api/2/issue/15572/comment/13779

jira-issue-add-link


Creates (or updates) an issue link.

Base Command

jira-issue-add-link

Input

Argument Name Description Required
globalId If a globalId is provided, and a remote issue link exists with that globalId, the remote issue link is updated. Optional
relationship The object relationship to issue, for example: causes. Optional
url The URL link. Required
title The title of the link. Required
summary The summary of the link. Optional
issueId The ID of the issue. Required
applicationType The application type of the linked remote application. E.g., “com.atlassian.confluence”. Optional
applicationName The application name of the linked remote application. E.g., “My Confluence Instance”. Optional

Context Output

There is no context output for this command.

Command Example

!jira-issue-add-link issueId=15572 title=test url=https://www.demisto.com/

Human Readable Output

jira-issue-add-link

id ticket_link
13722 https://demistodev.atlassian.net/rest/api/latest/issue/DEM-5415/remotelink/13722

jira-edit-issue


Modifies an issue in Jira.

Base Command

jira-edit-issue

Input

Argument Name Description Required
issueId The ID of the issue to edit. Required
issueJson The issue object (in JSON format). E.g., {“fields”:{“customfield_10037”: “field_value”}}. Optional
summary The summary of the issue. Optional
description The description of the issue. Optional
labels A CSV list of labels. Optional
priority The issue priority, for example: High, Medium. Optional
dueDate The due date for the issue (in the format 2018-03-11). Optional
assignee The name of the assignee. Relevant for Jira Server only. If you are using Jira Cloud, please provide the assignee_id argument instead. Optional
assignee_id The account ID of the assignee. Use the jira-get-id-by-attribute command to get the user’s account ID. Optional
status The issue status. Optional
transition The issue transition. Optional
environment A text field for describing the environment in which the issue occurred (for example - environment=”IE9 on Windows 7”). Optional
security The security level name of the issue, (for example - security=”Anyone”). Optional
components The components names of the issue, (for example - components=”component1,component2”). Optional

Context Output

Path Type Description
Ticket.Id Unknown The ID of the ticket.
Ticket.Key Unknown The key of the ticket.
Ticket.Assignee Unknown The user assigned to the ticket.
Ticket.Creator Unknown The user who created the ticket.
Ticket.Summary Unknown The summary of the ticket.
Ticket.Status Unknown The status of the ticket.

Command Example

!jira-edit-issue issueId=DEM-5415 issueJson=`{"fields":{"description":"testing3"}}`

Context Example

{
    "Ticket": {
        "Assignee": "{assignee}",
        "Creator": "{creator}",
        "Id": "10044",
        "Key": "DEM-5415",
        "Status": "To Do",
        "Summary": "Phishing Incident Declared",
        "attachment": ""
    }
}

Human Readable Output

jira-edit-issue

assignee attachment created creator description duedate id issueType key labels priority project reporter status summary ticket_link
{assignee}   2021-06-02T10:45:15.838-0400 {creator} testing3   10044 A small, distinct piece of work. DEM-5415   Medium SomethingGreat {reporter} To Do Phishing Incident Declared https://somejira.atlassian.net/rest/api/latest/issue/10044

Issue #DEM-5415 was updated successfully

jira-get-comments


Returns the comments added to a ticket.

Base Command

jira-get-comments

Input

Argument Name Description Required
issueId The ID of the issue from which to get the comments. Required

Context Output

Path Type Description
Ticket.Comment.Comment string The text of the comment.
Ticket.Comment.Created string The issue creation date.
Ticket.Comment.User string The user that created the comment.
Command Example

!jira-get-comments issueId=15572

Context Example
{
    "Ticket": {
        "Comment": [
            {
                "Comment": "test comment", 
                "User": "admin", 
                "Created": "2020-01-19T12:35:49.194+0200"
            }
        ], 
        "Id": "15572"
    }
}
Human Readable Output

Comments

Comment Created User
test comment 2020-01-19T12:35:49.194+0200 admin

jira-delete-issue


Deletes an issue in Jira.

Base Command

jira-delete-issue

Input

Argument Name Description Required
issueIdOrKey The ID or key of the issue. Required

Context Output

There is no context output for this command.

Command Example

!jira-delete-issue issueIdOrKey=DEM-5415

Human Readable Output

Issue deleted successfully.

jira-get-id-offset


Returns the ID offset, for example, the first issue ID.

Base Command

jira-get-id-offset

Input

There are no input arguments for this command.

Context Output

Path Type Description
Ticket.idOffSet string The ID offset.
Command Example

##### Context Example

{
“Ticket.idOffSet”: “10161”
}


##### Human Readable Output

ID Offset: 10161

### jira-get-id-by-attribute

***
Gets the Account ID for a given user's attribute.

#### Base Command

`jira-get-id-by-attribute`

#### Input

| **Argument Name** | **Description** | **Required** |
| --- | --- | --- |
| attribute | The user's attribute value. Can be Username or Email address. | Required |
| max_results | The maximum number of users to pull when searching for a matching user (default is 50). The maximum allowed value is dictated by the JIRA property 'jira.search.views.default.max'. If you specify a value that is higher than this number, your search results will be truncated. | Optional |
| is_jirav2api | Whether the server is on prem and uses the REST v2 API. | Optional |

#### Context Output

| **Path** | **Type** | **Description** |
| --- | --- | --- |
| Jira.User.Attribute | String | The user's attribute. |
| Jira.User.AccountID | String | The user's Account ID. |

#### Command Example

```!jira-get-id-by-attribute attribute="XSOAR User"```

#### Context Example

```json
{
    "Jira": {
        "User": {
            "AccountID": "5e4ds952052b790c97509a7c",
            "Attribute": "XSOAR User"
        }
    }
}

Human Readable Output

Account ID for attribute: XSOAR User is: 5e4ds952052b790c97509a7c

jira-list-transitions


Lists all possible transitions for a given ticket.

jira-append-to-field


Modifies a specific field in an issue in Jira by appending to it instead of replacing its content.
Field must be either of type string (appending by using ‘,’) or arrayd.

Base Command

jira-append-to-field

Input

Argument Name Description Required
issueId The ID of the issue to edit. Required
fieldJson The field object (in JSON format). For example {“customfield_10037”: “New value”}. Required

Context Output

Path Type Description
Ticket.Id Unknown The ticket ID.
Ticket.Key Unknown The ticket key.
Ticket.Assignee Unknown The user assigned to the ticket.
Ticket.Creator Unknown The user who created the ticket.
Ticket.Summary Unknown The ticket summary.
Ticket.Status Unknown The ticket status.

Command Example

!jira-append-to-field issueId=CIAC-3597 fieldJson={\"customfield_16492\":\"example\"}

Context Example

{
  "Ticket": {
    "Assignee": "User Name(user@example.com)",
    "Created": "2022-07-21T10:52:22.043+0000",
    "Creator": "User Name(user@example.com)",
    "DueDate": null,
    "Custom Field Display Name": "test,example",
    "Id": "1179420",
    "Key": "TEST-3597",
    "Labels": [
      "test",
    ],
    "LastSeen": null,
    "LastUpdate": "2022-07-27T04:47:24.214+0000",
    "Priority": "P5",
    "ProjectName": "Project Name",
    "Status": "Backlog",
    "Summary": "example",
    "attachment": ""
  }
}

jira-get-specific-field


Gets specific fields from a Jira issue and adds it to context dynamically.

Base Command

jira-get-specific-field

Input

Argument Name Description Required
issueId The ID of the issue to edit. Required
components The fields to retrieve from the issue. For example field=”customfield_164,labels”. Optional

Context Output

Path Type Description
Ticket.Id Unknown The ticket ID.
Ticket.Key Unknown The ticket key.
Ticket.Assignee Unknown The user assigned to the ticket.
Ticket.Creator Unknown The user who created the ticket.
Ticket.Summary Unknown The ticket summary.
Ticket.Status Unknown The ticket status.

Command Example

!jira-get-specific-field issueId="TEST-3597" field="labels,customfield_16492"

Context Example

{
  "Ticket": {
    "Assignee": "User Name(user@example.com)",
    "Created": "2022-07-21T10:52:22.043+0000",
    "Creator": "User Name(user@example.com)",
    "DueDate": null,
    "Custom Field Display Name": "test",
    "Id": "1179420",
    "Key": "TEST-3597",
    "Labels": [
      "test"
    ],
    "LastSeen": null,
    "LastUpdate": "2022-07-27T04:47:24.214+0000",
    "Priority": "P5",
    "ProjectName": "Project Name",
    "Status": "Backlog",
    "Summary": "example",
    "attachment": ""
  }
}

Base Command

jira-list-transitions

Input

Argument Name Description Required
issueId The ID of the issue. Required

Context Output

Path Type Description
Ticket.Transitions.ticketId Number The ID of the issue.
Ticket.Transitions.transitions Unknown A list of all possible transitions.

Command Example

!jira-list-transitions issueId=18847

Context Example

{
    "Ticket": {
        "Transitions": {
            "ticketId": "18847",
            "transitions": ["Backlog", "Selected for Development", "In Progress", "Done", "To Do", "Build Broken"]
        }
    }
}

Human Readable Output

List Transitions:

Transition Name
Backlog
Selected for Development
In Progress
Done
To Do
Build Broken

get-remote-data


Gets remote data from a remote incident. This method is only used for debugging purposes and will not update the current incident.

Base Command

get-remote-data

Input

Argument Name Description Required
id The remote incident ID (issue ID). Required
lastUpdate UTC timestamp in seconds. The incident is only updated if it was modified after the last update time. Default is 0. Optional

Context Output

There is no context output for this command.

get-mapping-fields


Returns the fields to map. This method is only used for debugging purposes.

Base Command

get-mapping-fields

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

update-remote-system


Gets incident changes. This method is only used for debugging purposes and will not update the current incident.

Base Command

update-remote-system

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

get-modified-remote-data


Available from Cortex XSOAR version 6.1.0. This command queries for incidents that were modified since the last update. This method is only used for debugging purposes.

Base Command

get-modified-remote-data

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

jira-issue-assign


Edit the assignee of an existing issue.

Base Command

jira-issue-assign

Input

Argument Name Description Required
issueId The ID of the issue to edit. Required
assignee The name of the assignee. Relevant for Jira Server only, if you are using Jira Cloud, provide the assignee_id argument instead. Optional
assignee_id The account ID of the assignee. Use the jira-get-id-by-attribute command to get the user’s account ID. Optional

Context Output

Path Type Description
Ticket.Id String The ticket ID.
Ticket.Key String The ticket key.
Ticket.Assignee String The user assigned to the ticket.
Ticket.Creator String The user who created the ticket.
Ticket.Summary String The ticket summary.
Ticket.Status String The ticket status.

Command example

!jira-issue-assign issueId=21492 assignee_id=1234

Context Example

{
    "Ticket": {
        "Assignee": "assignee1(null)",
        "Created": "2023-03-01T14:05:49.037+0200",
        "Creator": "assignee1(null)",
        "Description": null,
        "DueDate": null,
        "Id": "21492",
        "Key": "key",
        "Labels": [],
        "LastSeen": "2023-03-15T15:40:44.329+0200",
        "LastUpdate": "2023-05-03T16:15:32.771+0300",
        "Priority": "Medium",
        "ProjectName": "test",
        "Status": "To Do",
        "Summary": "something something",
        "attachment": "attachments"
    }
}

Human Readable Output

jira-issue-assign

|assignee| attachment |created|creator|description|duedate|id|issueType| key |labels|priority| project |reporter|status|summary| ticket_link |
|————-|—|—|—|—|—|—|—–|—|—|———|—|—|—|——————————————————-|—|
| assignee1(null) | attachments | 2023-03-01T14:05:49.037+0200 | assignee1(null) | | | 21492 | Task (Tasks track small, distinct pieces of work.) | key | | Medium | test | assignee1(null) | To Do | something something | https://test/rest/api/latest/issue/21492 |
Issue #21492 was updated successfully

Configure Incident Mirroring

This feature is compliant with Cortex XSOAR version 6.0 and above.
This part walks you through setting up the Jira integration to mirror incidents from Jira in Cortex XSOAR.
The instructions below include steps for configuring the integration and the incoming and outgoing mappers. However, not every option available in the integration, nor all classification and mapping features are covered.
For information about Classification and Mapping visit: Classification and Mapping (Cortex XSOAR 6.13) or Classification and Mapping (Cortex XSOAR 8 Cloud) or Classification and Mapping (Cortex XSOAR 8.7 On-prem).

When mirroring incidents, you can make changes in Jira, which will be reflected in Cortex XSOAR, or vice versa.
You can also attach files from either of the systems, which will then be available in the other system.

This is made possible by the addition of 3 new functions to the integration, which are applied with the following options:

  • External schema support
  • Can sync mirror in
  • Can sync mirror out

image

STEP 1 - Modify the incoming mapper

  1. Navigate to Classification and Mapping and click classifier-mapper-incoming-JiraV2.
  2. Under the Incident Type dropdown, select Jira Incident.
  3. Change the mapping according to your needs.
  4. Save your changes.
5 fields have been added to support the mirroring feature
  • dbotMirrorDirection - determines whether mirroring is incoming, outgoing, or both. Default is Both.
    • You can choose the mirror direction when configuring the Jira instance using the Incident Mirroring Direction field.
  • dbotMirrorId - determines the incident ID in the 3rd party integration. In this case, the Jira ID field.
  • dbotMirrorInstance - determines the Jira instance with which to mirror.
  • dbotMirrorLastSync - determines the field by which to indicate the last time that the systems synchronized.
  • dbotMirrorTags - determines the tags that you need to add in Cortex XSOAR for entries to be pushed to Jira.
    • You can set the tags in the instance configuration, using File Entry Tag, and Comment Entry Tag.

image

STEP 2 - Modify the outgoing mapper

  1. Under Classification and Mapping, click classifier-mapper-outgoing-Jira.
  2. Under Select Instance dropdown, select the instance name you want to work with.
    The left side of the screen shows the Jira fields to map and the right side of the
    screen shows the Cortex XSOAR fields by which you are mapping.
    *Note: If Select Instance dropdown is empty, go to the integration’s settings and under Incident Type select Jira Incident and try again.
  3. Under Schema Type, select Jira Incident. The Schema Type represents the Jira entity that
    you are mapping to. In our example it is an incident, but it can also be any other kind of ticket that
    Jira supports.
  4. Under the Incident Type dropdown, select Jira Incident.
  5. On the right side of the screen, under Incident, select the incident based on which you want to
    match.
  6. Change the mapping according to your needs.
  7. Save your changes.

STEP 3 - Configure the following integration fields in order to customize the mirroring feature

  1. Mirror outgoing incidents: If enabled, any incident data changed in existing fetched incidents will be reflected in the remote Jira server.
  2. Mirror incoming incidents: If enabled, any incident data changed in the remote Jira server will be reflected in existing fetched incidents.
  3. Fetch incidents: Should be enabled in order to mirror in and out new incidents.
  4. Incident type: In order to mirror out changes, provide an incident type that is associated with a layout containing the fields, which values you want to mirror. You can use ‘Jira Incident’, which already has a built-in layout.
  5. File Entry Tag: Choose a tag to add to an entry in order to mirror it as an attachment in Jira.
  6. Comment Entry Tag: Choose a tag to add to an entry in order to mirror it as a comment in Jira.
  7. Fetch Comments: Fetch comments for Jira ticket.
  8. Fetch Attachments: Fetch attachments for Jira ticket.

STEP 4 - Create an incident in Jira. For the purpose of this use case, it can be a very simple incident

STEP 5 - In Cortex XSOAR, the new ticket will be ingested in approximately one minute

  1. Add a note to the incident. In the example below, we have written a comment from Cortex XSOAR to Jira.
  2. Click Actions -> Tags and add the Comment Entry Tag tag that you’ve selected before. If not already modified, the default is: “comment”.
  3. Add a file to the incident and mark it with the File Entry Tag tag that you’ve selected before. If not already modified, the default is: “attachment”.
    image
  4. Go back to Incident Info and locate a field you’ve configured in the outgoing mapper that is displayed in the incident’s layout. Change its value and click ‘V’ in the ‘V/X’ menu to save the changes.
    image
  5. Navigate back to the incident in Jira and within approximately one minute, the changes will be reflected there as well.
  6. Change a Jira field you’ve configured in the incoming mapper.
  7. Go back to Cortex XSOAR and within approximately one minute, the changes will be reflected there as well.
  • You can make additional changes like closing the incident or changing the description and those will be reflected in both systems.

Notes

  • The final ‘source of truth’ of the incident for Cortex XSOAR are the values in Cortex XSOAR.
    Meaning, if you change the severity in Cortex XSOAR and then change it back in Jira, the final value that will be presented is the one in Cortex XSOAR.
    You can see a list of these fields for each incident under “Context Data” -> “dbotDirtyFields”.
  • If you wish to mirror in and out Jira’s custom fields, please see the ‘Mirror In And Out Custom Fields’ section below.
  • If you wish to change the status of a Jira incident using transitions, please see the ‘Change Ticket’s Status Using Transitions’ section below.

Mirror In And Out Custom Fields

Add a new custom field and add it to the incident type's layout

Add a new custom field and add it to the incident type's layout:

1.Create a new incident field:

  1. Go to settings -> advanced -> fields -> new field
  2. Under “Basic Settings”:
    a.provide a name
    b.Choose field type. For example for a label, you might want to use “Tag” as a type.
  3. Click on “Attributes”:
    a.Uncheck the box under: “Add to incident types”
    b.for “Add associated type…” Select the incident type you want to work with. In this example I’ll choose “Jira Incident”.

2.Add the new incident field you've just created to the layout associated with the integration's incident type. In this example, the layout is “Jira Incident Layout”:

Add the new field to the incoming mapper

  1. Go to Settings -> Integrations -> Servers & Services -> in the search bar type “Jira”.
  2. Open the integration settings by clicking on the button: or by clicking on “Add instance” if you don't have Jira already configured in your system.
    1.If you don't have Jira configured yet, please provide all the needed information for authentication (Jira URL, Username if needed…) and click the “Test” button in order to see that the integration is well set.
    2.In addition:
    • Make sure that under “Incident type” you've selected the incident type you want to work with and it is the same one you've selected once you created the new field. In our example: “Jira Incident”.
    • The option “Fetch Incidents” is checked.
    • The option “Mirror incoming incidents” is checked.
    • The option “Mirror outgoing incidents” is checked.

Finally, click “Done”, and wait for new Jira incidents to be pulled:

  1. Once you've finished the setup stage, go to the “Mapper (incoming)” field and select the mapper you want to work with by clicking on “Select”:

  2. Click on the button: in order to edit the selected mapper. It will open the “Incident Incoming Mapping Editor”.
    1.For “Incident Type”, please provide the incident type you've selected in the integration settings.
    2.In “Select Instance” choose the name of your instance. You can find it in the integration settings (you can see a screenshot above), under “Name”.
    Once done, You'll see on the right side of the screen a Jira incident.
    *Note: If you want to work with a specific incident, in “Get data” select the option: “Upload JSON” and provide the wanted incident as JSON.
    In order to get the incident as JSON, you can see the section: “how to get an incident as JSON” below.
    On the left side, You'll see all Jira incident fields.

    3.Use “Search Cortex XSOAR field…” in order to find your new incident field by searching its name.
    4.Once found, click on “Choose data path” and map it to the custom jira field:
    a.Find the jira field you want to map to this incident field on the right side, and click on its value.
    b.Then you will see the path you've selected under your new added field:

    *Note: In this example:”customfield_ 10045” is the Jira field ID we want to map. You can find IDs of custom fields for your field in Jira using this guide:Click Here
    *Note: You can also type the path manually.
  3. Click “Save Version”.

Add the new field to the outgoing mapper

  1. Go to Settings -> Integrations -> Servers & Services -> in the search bar type “Jira”.
  2. Open the integration settings by clicking on the button:
  3. go to the “Mapper (outgoing)” field and select the mapper you want to work with by clicking on “Select” (the same process we did with the incoming mapper).
  4. Click on the button: in order to edit the selected mapper. It will open the “Incident Outgoing Mapping Editor”.
  5. In “Select Instance” choose the name of your instance. You can find it in the integration settings (you can see a screenshot above), under “Name”.
    1.See that “Schema Type” changes automatically to “Jira Incident”.
  6. Select one incident to work with by specifying it in the “Incident” list (it can be seen in the screenshot above under “Select Instance”).
    1.On the right side you are now able to see your selected incident.
    2.On the left side you are able to see all the Jira fields you can map in the system.
  7. Search for the Jira field you want to map on the left side. In this example, we want to map customfield_10045
  8. Map the Jira field to the incident field you've created by clicking on “Select incident field” and type its name*.
    *The name of the incident will not include spaces, underscores, capital letters, etc. for example: Jira Transitions will be transformed to “jiratransitions” once saved in the incident's context (you can see the name you need to select by fetching incidents after configuring the mapper in. In the “Incident” field select one of the new incidents that the edited mapper in has worked on. On the right side you should be able to see the new added field and the name you need to provide).
  9. Click the “Save Version” button.

How to get an incident as JSON

  1. Use the !jira-get-issue command with the raw-response=true parameter. For example: !jira-get-issue issueId=VIK-183 raw-response=true
  2. Click on “view full JSON in a new tab”
  3. Click on “Download as a file”.
  4. Open the downloaded file and put its content inside a list, resulting in a list which contains only one item, the downloaded JSON.
  5. When selecting “Upload JSON” for “Get data” as mentioned above, you can upload the file using the “Upload JSON” option:

Change Ticket’s Status Using Transitions

  1. Make sure you’ve configured your instance as described in ‘Configure Incident Mirroring’ above.
  2. Make sure you’re using the ‘Jira Incident Layout’ for displaying the incident which status you want to change.
    The reason this is required is that this layout is structured in a way that both ‘script-JiraChangeTransition’ and ‘script-JiraListTransition’ scripts can work, which is crucial when you want to use transitions.
  3. In order to change an incident status, open the incident you would like to update.
  4. Under “Incident Info” search for the “Jira Transitions” field.
  5. Click on “Select” and choose the name of the new status.
  6. Click on the ‘V’ option in the ‘V/X’ menu.
  7. You can see the new status under ‘Jira Status’.
  8. Go to Jira, within approximately one minute you will be able to see the new status there as well.

jira-add-user-to-project


Adds the user to a project.

Base Command

jira-add-user-to-project

Input

Argument Name Description Required
user_email User’s email address. Optional
project_key Project key. Optional
role_name Role name to assign the user. Optional

Context Output

There is no context output for this command.

jira-get-project-role


Gets the information related to the role, including assigned users.

Base Command

jira-get-project-role

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

There is no context output for this command.

jira-get-organizations


Lists all the organizations.

Base Command

jira-get-organizations

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

There is no context output for this command.

Configuration parameters

  • url — Jira URL, for example: https://demisto.atlassian.net/ (required)
  • username — Username (Basic Authentication)
  • password — Password (Deprecated - Use API token)
  • credentials
  • consumerKey — Consumer key (OAuth 1.0)
  • accessToken — Access Token
  • privateKey — Private key (PKCS8 format)
  • query — Query (in JQL) for fetching incidents.
  • idOffset — Issue index to start fetching incidents from
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • isFetch — Fetch incidents
  • incoming_mirror — Mirror incoming incidents
  • incidentType — Incident type
  • fetchByCreated — Use created field to fetch incidents
  • outgoing_mirror — Mirror outgoing incidents
  • file_tag — File entry tag
  • comment_tag — Comment entry tag
  • fetch_comments — Fetch comments
  • fetch_attachments — Fetch attachments
  • APItoken — API token (Deprecated)
  • incidentFetchInterval — Incidents Fetch Interval

Commands (22)

  • get-mapping-fields

    Returns the fields to map. This method is only used for debugging.

  • get-modified-remote-data

    Available from Cortex XSOAR version 6.1.0. This command queries for incidents that were modified since the last update. This method is only used for debugging.

  • get-remote-data

    Gets remote data from a remote incident. This method is only used for debugging and does not update the current incident.

  • jira-add-user-to-project

    Adds the user to a project.

  • jira-append-to-field

    Modifies a specific field in an issue in Jira by appending to it instead of replacing its content. Field must be either of type string (appending by using ',') or array.

  • jira-create-issue

    Creates a new issue in Jira.

  • jira-delete-issue

    Deletes an issue in Jira.

  • jira-edit-issue

    Modifies an issue in Jira.

  • jira-get-comments

    Returns the comments added to a ticket.

  • jira-get-id-by-attribute

    Gets the account ID for a given user attribute.

  • jira-get-id-offset

    Returns the ID offset. For example the first issue ID.

  • jira-get-issue

    Fetches an issue from Jira.

  • jira-get-organizations

    Lists all the organizations.

  • jira-get-project-role

    Gets the information related to the role, including assigned users.

  • jira-get-specific-field

    Gets specific fields from a Jira issue and adds it to context dynamically.

  • jira-issue-add-comment

    Adds a new comment to an existing Jira issue.

  • jira-issue-add-link

    Creates (or updates) an issue link.

  • jira-issue-assign

    Edit the assignee of an existing issue.

  • jira-issue-query

    Queries Jira issues.

  • jira-issue-upload-file

    Uploads a file attachment to an issue.

  • jira-list-transitions

    Lists all possible transitions for a given ticket.

  • update-remote-system

    Gets incident changes. This method is only used for debugging and does not update the current incident.

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401


from requests_oauthlib import OAuth1
from dateparser import parse
from datetime import timedelta

import urllib3
urllib3.disable_warnings()

''' GLOBALS/PARAMS '''
BASE_URL = demisto.getParam('url').rstrip('/') + '/'
API_TOKEN = demisto.getParam('APItoken') or (demisto.getParam('credentials') or {}).get('password')
USERNAME = demisto.getParam('username')
PASSWORD = demisto.getParam('password')
COMMAND_NOT_IMPELEMENTED_MSG = 'Command not implemented'

HEADERS = {
    'Content-Type': 'application/json',
}
JIRA_INCIDENT_TYPE_NAME = 'Jira Incident'
ISSUE_INCIDENT_FIELDS = {'issueId': 'The ID of the issue to edit',
                         'summary': 'The summary of the issue.',
                         'description': 'The description of the issue.',
                         'labels': 'A CSV list of labels.',
                         'priority': 'A priority name, for example "High" or "Medium".',
                         'dueDate': 'The due date for the issue (in the format 2018-03-11).',
                         'assignee': 'The name of the assignee.',
                         'status': 'The name of the status.',
                         'assignee_id': 'The account ID of the assignee. Use'
                                        ' the jira-get-id-by-attribute command to get the user\'s Account ID.'
                         }
BASIC_AUTH_ERROR_MSG = "For cloud users: As of June 2019, Basic authentication with passwords for Jira is no" \
                       " longer supported, please use an API Token or OAuth 1.0"
JIRA_RESOLVE_REASON = 'Issue was marked as "Done"'
USE_SSL = not demisto.params().get('insecure', False)

SESSION = requests.Session()
SESSION.mount(prefix='https://', adapter=SSLAdapter(verify=USE_SSL))


def jira_req(
        method: str,
        resource_url: str,
        body: str = '',
        link: bool = False,
        resp_type: str = 'text',
        headers: dict | None = None,
        files: dict | None = None,
        params: dict | None = None
):
    url = resource_url if link else (BASE_URL + resource_url)
    AUTH = get_auth()
    if headers and HEADERS.get('Authorization'):
        headers['Authorization'] = HEADERS.get('Authorization')
    try:

        result = SESSION.request(
            method=method,
            url=url,
            data=body,
            auth=AUTH,
            headers=headers if headers else HEADERS,
            verify=USE_SSL,
            files=files,
            params=params
        )
    except ValueError:
        raise ValueError("Could not deserialize privateKey")

    if not result.ok:
        demisto.debug(result.text)
        try:
            rj = result.json()
            if rj.get('errorMessages'):
                raise DemistoException(f'Status code: {result.status_code}\nMessages: {", ".join(rj["errorMessages"])}')
            elif errors := rj.get('errors'):
                error_messages = []
                if isinstance(errors, list):
                    for error in errors:
                        error_messages.append(", ".join(error.values()))
                else:
                    error_messages.append(", ".join(errors.values()))
                raise DemistoException(f'Status code: {result.status_code}\nMessages: {error_messages}')
            else:
                raise DemistoException(f'Status code: {result.status_code}\nError text: {result.text}')
        except ValueError as ve:
            demisto.debug(str(ve))
            if result.status_code == 401:
                raise DemistoException('Unauthorized request, please check authentication related parameters.'
                                       f'{BASIC_AUTH_ERROR_MSG}')
            elif result.status_code == 404:
                raise DemistoException("Could not connect to the Jira server. Verify that the server URL is correct.")
            elif result.status_code == 500 and files:
                raise DemistoException(f"Failed to execute request, status code: 500\nBody: {result.text}"
                                       f"\nMake sure file name doesn't contain any special characters")
            else:
                raise DemistoException(
                    f"Failed reaching the server. status code: {result.status_code}")

    if resp_type == 'json':
        return result.json()
    return result


def generate_oauth1():
    oauth = OAuth1(
        client_key=demisto.getParam('consumerKey'),
        rsa_key=demisto.getParam('privateKey'),
        signature_method='RSA-SHA1',
        resource_owner_key=demisto.getParam('accessToken'),
    )
    return oauth


def generate_basic_oauth():
    return USERNAME, (API_TOKEN or PASSWORD)


def get_auth():
    access_token = demisto.getParam('accessToken')
    is_basic = USERNAME and (PASSWORD or API_TOKEN)
    is_oauth1 = demisto.getParam('consumerKey') and access_token and demisto.getParam('privateKey')
    is_bearer = access_token and not is_oauth1

    if is_basic:
        return generate_basic_oauth()

    elif is_oauth1:
        HEADERS.update({'X-Atlassian-Token': 'nocheck'})
        return generate_oauth1()

    elif is_bearer:
        # Personal Access Token Authentication
        HEADERS.update({'Authorization': f'Bearer {access_token}'})
        return None

    return_error(
        'Please provide the required Authorization information:'
        '- Basic Authentication requires user name and password or API token'
        '- OAuth 1.0 requires ConsumerKey, AccessToken and PrivateKey'
        '- Personal Access Tokens requires AccessToken'
    )
    return None


def get_custom_field_names():
    """
    This function returns all custom fields.
    :return: dict of custom fields: id as key and name as value.
    """
    custom_id_name_mapping = {}
    HEADERS['Accept'] = "application/json"
    try:
        res = jira_req(method='GET', resource_url='rest/api/latest/field', headers=HEADERS)
    except Exception as e:
        demisto.error(f'Could not get custom fields because got the next exception: {e}')
    else:
        if res.ok:
            custom_fields_list = res.json()
            custom_id_name_mapping = {field.get('id'): field.get('name') for field in custom_fields_list}
        else:
            demisto.error(f'Could not get custom fields. status code: {res.status_code}. reason: {res.reason}')
    finally:
        return custom_id_name_mapping


def run_query(query, start_at='', max_results=None, extra_fields=None, nofields=None):
    # EXAMPLE
    """
    request = {
        "jql": "project = HSP",
        "startAt": 0,
        "maxResults": 15,
        "fields": [    <-- not supported yet, but easily attainable
            "summary",
            "status",
            "assignee"
        ]
    }
    """
    demisto.debug(f'querying with: {query}')
    url = 'rest/api/latest/search/'
    query_params = {
        'jql': query,
        "startAt": start_at,
        "maxResults": max_results,
    }
    if extra_fields:
        fields = extra_fields.split(",")
        fields_mapping_name_id = {k.lower(): v.lower() for k, v in get_custom_field_names().items()}
        query_params['fields'] = [k for y in fields for k, v in fields_mapping_name_id.items() if v == y.lower()]
        nofields.update(
            {fieldextra for fieldextra in fields if fieldextra.lower() not in fields_mapping_name_id.values()})
    if nofields:
        if len(nofields) > 1:
            return_warning(f'{",".join(nofields)} do not exist')
        else:
            return_warning(f'{",".join(nofields)} does not exist')
    try:
        result = jira_req(method='GET', resource_url=url, headers=HEADERS, params=query_params)
    except ValueError:
        raise ValueError("Could not deserialize privateKey")

    try:
        rj = result.json()
        if rj.get('issues'):
            return rj

        errors = ",".join(rj.get("errorMessages", ['could not fetch any issues, please check your query']))
        if 'could not fetch any issues, please check your query' in errors:
            return {}
        raise Exception(f'No issues were found, error message from Jira: {errors}')

    except ValueError as ve:
        demisto.debug(str(ve))
        raise Exception(f'Failed to send request, reason: {result.reason}')


def get_id_offset():
    """
    gets the ID Offset, i.e., the first issue id. used to fetch correctly all issues
    """
    query = "ORDER BY created ASC"
    j_res = run_query(query=query, max_results=1)
    first_issue_id = j_res.get('issues')[0].get('id')
    return_outputs(
        readable_output=f"ID Offset: {first_issue_id}",
        outputs={'Ticket.idOffSet': first_issue_id},
    )


def get_custom_fields():
    """
    This function returns all custom fields.
    :return: dict of custom fields: id as key and description as value.
    """
    custom_id_description_mapping = {}
    HEADERS['Accept'] = "application/json"
    try:
        res = jira_req(method='GET', resource_url=BASE_URL + 'rest/api/latest/field', headers=HEADERS)
    except Exception as e:
        demisto.error(f'Could not get custom fields because got the next exception: {e}')
    else:
        if res.status_code == 200:
            custom_fields_list = res.json()
            custom_id_description_mapping = {field.get('id'): field.get('description') for field in custom_fields_list}
        else:
            demisto.error(f'Could not get custom fields. status code: {res.status_code}. reason: {res.reason}')
    finally:
        return custom_id_description_mapping


def expand_urls(data, depth=0):
    if isinstance(data, dict) and depth < 10:
        for key, value in data.items():
            if key in ['_links', 'watchers', 'sla', 'request participants']:
                # dictionary of links
                if isinstance(value, dict):
                    for link_key, link_url in value.items():
                        value[link_key + '_expended'] = json.dumps(
                            jira_req(method='GET', resource_url=link_url, link=True, resp_type='json'))
                # link
                else:
                    data[key + '_expended'] = json.dumps(jira_req(method='GET', resource_url=value,
                                                                  link=True, resp_type='json'))
            # search deeper
            else:
                if isinstance(value, dict):
                    return expand_urls(value, depth + 1)
        return None
    return None


def search_user(query: str, max_results: str = '50', is_jirav2api: bool = False):
    """
        Search for user by name or email address.
    Args:
        query: A query string that is matched against user attributes ( displayName, and emailAddress) to find relevant users.
        max_results (str): The maximum number of items to return. default by the server: 50
        is_jirav2api (bool): if the instance is connecting to a Jira Server that supports only the v2 REST API. default as False


    Returns:
        List of users.
    """

    if is_jirav2api:
        """
        override user identifier for Jira v2 API
        https://docs.atlassian.com/software/jira/docs/api/REST/8.13.15/#user-findUsers
        """
        url = f"rest/api/latest/user/search?username={query}&maxResults={max_results}"
    else:
        url = f"rest/api/latest/user/search?query={query}&maxResults={max_results}"

    res = jira_req('GET', url, resp_type='json')
    return res


def get_account_id_from_attribute(
        attribute: str,
        max_results: str = '50',
        is_jirav2api: str = 'false') -> CommandResults | str:
    """
    https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-user-search/#api-rest-api-3-user-search-get

    Args:
        attribute (str): Username or Email address of a user.
        max_results (str): The maximum number of items to return. default by the server: 50
        is_jirav2api (str): if the instance is connecting to a Jira Server that supports only the v2 REST API. default as false
    """

    if is_jirav2api == 'true':
        """
        override user identifier for Jira v2 API
        https://docs.atlassian.com/software/jira/docs/api/REST/8.13.15/#user-findUsers
        """
        users = list(search_user(attribute, max_results, is_jirav2api=True))
        account_ids = {
            user.get('name') for user in users if (attribute.lower() in [user.get('displayName', '').lower(),
                                                                         user.get('emailAddress', '').lower()])}
    else:
        users = list(search_user(attribute, max_results))
        account_ids = {
            user.get('accountId') for user in users if (attribute.lower() in [user.get('displayName', '').lower(),
                                                                              user.get('emailAddress', '').lower()])}

    if not account_ids:
        # The email address is a private account field and sometimes is blank. If there is only one result,
        # then it is the one. If there are more results for the query, the user should try "DisplayName" attribute.
        if not users:
            return f'No Account ID was found for attribute: {attribute}.'
        if len(users) == 1:
            account_ids = {users[0].get('name')} if is_jirav2api == 'true' else {users[0].get('accountId')}
        else:
            demisto.debug(f'Multiple account IDs found, but it was not possible to resolve which one of them is most '
                          f'relevant to attribute \"{attribute}\". Account ids: {account_ids}')
            return f'Multiple account IDs found, but it was not possible to resolve which one of them is most ' \
                   f'relevant to attribute \"{attribute}\".Please try to provide the "DisplayName" attribute.'

    if len(account_ids) > 1:
        return f'Multiple account IDs were found for attribute: {attribute}.\n' \
               f'Please try to provide the other attribute available - Email or DisplayName.'

    account_id = next(iter(account_ids))
    outputs = {
        'Attribute': attribute,
        'AccountID': account_id
    }

    return CommandResults(
        outputs_prefix='Jira.User',
        outputs_key_field='AccountID',
        readable_output=f'Account ID for attribute: {attribute} is: {account_id}',
        outputs=outputs,
    )


def generate_md_context_get_issue(data, customfields=None, nofields=None, extra_fields=None):
    get_issue_obj: dict = {"md": [], "context": []}
    if not isinstance(data, list):
        data = [data]

    for element in data:
        md_obj, context_obj = {}, {}

        context_obj['Id'] = md_obj['id'] = demisto.get(element, 'id')
        context_obj['Key'] = md_obj['key'] = demisto.get(element, 'key')
        context_obj['Summary'] = md_obj['summary'] = demisto.get(element, 'fields.summary')
        context_obj['Status'] = md_obj['status'] = demisto.get(element, 'fields.status.name')
        context_obj['Priority'] = md_obj['priority'] = demisto.get(element, 'fields.priority.name')
        context_obj['ProjectName'] = md_obj['project'] = demisto.get(element, 'fields.project.name')
        context_obj['DueDate'] = md_obj['duedate'] = demisto.get(element, 'fields.duedate')
        context_obj['Created'] = md_obj['created'] = demisto.get(element, 'fields.created')
        context_obj['Description'] = md_obj['description'] = demisto.get(element, 'fields.description')
        context_obj['Labels'] = md_obj['labels'] = demisto.get(element, 'fields.labels')

        # Parse custom fields into their original names
        custom_fields = [i for i in demisto.get(element, "fields") if "custom" in i]
        if extra_fields:
            custom_fields = extra_fields
        if (custom_fields and customfields and not nofields) or extra_fields:
            field_mappings = get_custom_field_names()
            for field_returned in custom_fields:
                readable_field_name = field_mappings.get(field_returned)
                if readable_field_name:
                    context_obj[readable_field_name] = md_obj[readable_field_name] = \
                        demisto.get(element, f"fields.{field_returned}")

        assignee = demisto.get(element, 'fields.assignee')
        context_obj['Assignee'] = md_obj['assignee'] = "{name}({email})".format(
            name=assignee.get('displayName', 'null'),
            email=assignee.get('emailAddress', 'null')
        ) if assignee else 'null(null)'

        creator = demisto.get(element, 'fields.creator')
        context_obj['Creator'] = md_obj['creator'] = "{name}({email})".format(
            name=creator.get('displayName', 'null'),
            email=creator.get('emailAddress', 'null')
        ) if creator else 'null(null)'

        reporter = demisto.get(element, 'fields.reporter')
        md_obj['reporter'] = "{name}({email})".format(
            name=reporter.get('displayName', 'null'),
            email=reporter.get('emailAddress', 'null')
        ) if reporter else 'null(null)'

        context_obj.update({
            'LastSeen': demisto.get(element, 'fields.lastViewed'),
            'LastUpdate': demisto.get(element, 'fields.updated'),
        })

        md_obj.update({
            'issueType': f"{demisto.get(element, 'fields.issuetype.name')} "
                         f"({demisto.get(element, 'fields.issuetype.description')})",
            'labels': demisto.get(element, 'fields.labels'),
            'description': demisto.get(element, 'fields.description'),
            'ticket_link': demisto.get(element, 'self'),
        })
        attachments = demisto.get(element, 'fields.attachment')
        if isinstance(attachments, list):
            md_obj['attachment'] = ','.join(attach.get('filename') for attach in attachments)
            context_obj['attachment'] = ','.join(attach.get('filename') for attach in attachments)

        get_issue_obj['md'].append(md_obj)
        get_issue_obj['context'].append(context_obj)

    return get_issue_obj


def generate_md_context_create_issue(data, project_name=None, project_key=None):
    create_issue_obj = {"md": [], "context": {"Ticket": []}}  # type: ignore
    if project_name:
        data["projectName"] = project_name

    if project_key:
        data["projectKey"] = project_key

    elif demisto.getParam('projectKey'):
        data["projectKey"] = demisto.getParam('projectKey')

    create_issue_obj['md'].append(data)  # type: ignore
    create_issue_obj['context']['Ticket'].append(  # type: ignore
        {"Id": demisto.get(data, 'id'), "Key": demisto.get(data, 'key')})  # type: ignore
    return create_issue_obj


def generate_md_upload_issue(data, issue_id):
    upload_md = []
    if not isinstance(data, list):
        data = [data]

    for element in data:
        md_obj = {
            'id': demisto.get(element, 'id'),
            'issueId': issue_id,
            'attachment_name': demisto.get(element, 'filename'),
            'attachment_link': demisto.get(element, 'self')
        }
        upload_md.append(md_obj)

    return upload_md


def get_mirror_type(should_mirror_in, should_mirror_out):
    """
    This function return the type of mirror to perform on a Jira incident.
    NOTE: in order to not mirror an incident, the type should be None.
    :param should_mirror_in: demisto.params().get("incoming_mirror")
    :param should_mirror_out: demisto.params().get('outgoing_mirror')
    :return: The mirror type
    """
    # Adding mirroring details
    mirror_type = None
    if should_mirror_in and should_mirror_out:
        mirror_type = 'Both'
    elif should_mirror_in:
        mirror_type = 'In'
    elif should_mirror_out:
        mirror_type = 'Out'
    return mirror_type


def create_incident_from_ticket(issue, should_get_attachments, should_get_comments, should_mirror_in, should_mirror_out,
                                comment_tag, attachment_tag):
    labels = [
        {'type': 'issue', 'value': json.dumps(issue)}, {'type': 'id', 'value': str(issue.get('id'))},
        {'type': 'lastViewed', 'value': str(demisto.get(issue, 'fields.lastViewed'))},
        {'type': 'priority', 'value': str(demisto.get(issue, 'fields.priority.name'))},
        {'type': 'status', 'value': str(demisto.get(issue, 'fields.status.name'))},
        {'type': 'project', 'value': str(demisto.get(issue, 'fields.project.name'))},
        {'type': 'updated', 'value': str(demisto.get(issue, 'fields.updated'))},
        {'type': 'reportername', 'value': str(demisto.get(issue, 'fields.reporter.displayName'))},
        {'type': 'reporteremail', 'value': str(demisto.get(issue, 'fields.reporter.emailAddress'))},
        {'type': 'created', 'value': str(demisto.get(issue, 'fields.created'))},
        {'type': 'summary', 'value': str(demisto.get(issue, 'fields.summary'))},
        {'type': 'description', 'value': str(demisto.get(issue, 'fields.description'))},

    ]

    name = demisto.get(issue, 'fields.summary')
    if name:
        name = f"Jira issue: {issue.get('id')}"

    severity = 0
    if demisto.get(issue, 'fields.priority') and demisto.get(issue, 'fields.priority.name'):
        if demisto.get(issue, 'fields.priority.name') == 'Highest':
            severity = 4
        elif demisto.get(issue, 'fields.priority.name') == 'High':
            severity = 3
        elif demisto.get(issue, 'fields.priority.name') == 'Medium':
            severity = 2
        elif demisto.get(issue, 'fields.priority.name') == 'Low':
            severity = 1

    file_names = []
    if should_get_attachments:
        for file_result in get_entries_for_fetched_incident(issue.get('id'), False, True)['attachments']:
            if file_result['Type'] != entryTypes['error']:
                file_names.append({
                    'path': file_result.get('FileID', ''),
                    'name': file_result.get('File', '')
                })

    if should_get_comments:
        labels.append({'type': 'comments', 'value': str(get_entries_for_fetched_incident(issue.get('id'), True, False)
                                                        ['comments'])})
    else:
        labels.append({'type': 'comments', 'value': '[]'})

    issue['mirror_direction'] = get_mirror_type(should_mirror_in, should_mirror_out)

    issue['mirror_tags'] = [
        comment_tag,
        attachment_tag
    ]
    issue['mirror_instance'] = demisto.integrationInstance()

    return {
        "name": name,
        "labels": labels,
        "details": demisto.get(issue, "fields.description"),
        "severity": severity,
        "attachment": file_names,
        "rawJSON": json.dumps(issue)
    }


def get_project_id(project_key='', project_name=''):
    if not project_key and not project_name:
        return_error('You must provide at least one of the following: project_key or project_name')

    result: dict = {}

    try:
        result = jira_req('GET', 'rest/api/latest/issue/createmeta', resp_type='json')
    except DemistoException as de:

        if 'Status code: 404' not in de.message:
            raise de

        demisto.debug('Exception after first api call: {de.message}')
        demisto.debug(f'Could not find expected Jira endpoint: {BASE_URL}/api/latest/issue/createmeta.'
                      f'Trying another endpoint: {BASE_URL}/api/latest/project.')

        # a new endpoint for Jira version 9.0.0 and above, so we execute another api call
        result = jira_req('GET', 'rest/api/latest/project', resp_type='json')

        # Jira's response changed to a list of projects from version 9.0.0
        projects_lst = list(filter(
            lambda x: x.get('key').lower() == project_key.lower() or x.get('name').lower() == project_name.lower(),
            result))

        # Filtering should give us a list with one project, only one project should match the filter's conditions
        if projects_lst:
            return projects_lst[0].get('id')

    # Jira used to respond with a dictionary with the 'projects' key until version 9.0.0
    if isinstance(result, dict):
        for project in result.get('projects', []):
            if project_key.lower() == project.get('key').lower() or project_name.lower() == project.get('name').lower():
                return project.get('id')

    return_error('Project not found')
    return None


def get_issue_fields(issue_creating=False, mirroring=False, **issue_args):
    """
    refactor issues's argument as received from demisto into jira acceptable format, and back.
    :param issue_creating: flag that indicates this function is called when creating an issue
    :param issue_args: issue argument
    """
    issue = {}  # type: dict
    if 'issue_json' in issue_args:
        try:
            issue = json.loads(issue_args['issue_json'], strict=False)
        except TypeError as te:
            demisto.debug(str(te))
            return_error("issueJson must be in a valid json format")
    elif 'issueJson' in issue_args:
        try:
            issue = json.loads(issue_args['issueJson'], strict=False)
        except TypeError as te:
            demisto.debug(str(te))
            return_error("issueJson must be in a valid json format")

    if not issue.get('fields'):
        issue['fields'] = {}

    if mirroring:
        for field_name in issue_args:
            if field_name and field_name.startswith('customfield'):
                issue['fields'][field_name] = issue_args[field_name]

    if not issue['fields'].get('issuetype') and issue_creating:
        issue['fields']['issuetype'] = {}

    if issue_args.get('watchers'):
        watchers = issue_args.get('watchers')
        if isinstance(watchers, list):
            issue['watchers'] = watchers
        elif isinstance(watchers, str):
            issue['watchers'] = argToList(watchers)

    if issue_args.get('summary'):
        issue['fields']['summary'] = issue_args['summary']

    if not issue['fields'].get('project') and (issue_args.get('projectKey') or issue_args.get('projectName')):
        issue['fields']['project'] = {}

    if issue_args.get('projectKey'):
        issue['fields']['project']['key'] = issue_args.get('projectKey', '')
    if issue_args.get('projectName'):
        issue['fields']['project']['name'] = issue_args.get('projectName', '')

    if issue_creating:
        # make sure the key & name are right, and get the corresponding project id & key
        project_id = get_project_id(issue['fields'].get('project', {}).get('key', ''),
                                    issue['fields'].get('project', {}).get('name', ''))
        issue['fields']['project']['id'] = project_id

    if issue_args.get('issueTypeName'):
        issue['fields']['issuetype']['name'] = issue_args['issueTypeName']

    if issue_args.get('issueTypeId'):
        issue['fields']['issuetype']['id'] = issue_args['issueTypeId']

    if issue_args.get('parentIssueId'):
        if not issue['fields'].get('parent'):
            issue['fields']['parent'] = {}
        issue['fields']['parent']['id'] = issue_args['parentIssueId']

    if issue_args.get('parentIssueKey'):
        if not issue['fields'].get('parent'):
            issue['fields']['parent'] = {}
        issue['fields']['parent']['key'] = issue_args['parentIssueKey']

    if issue_args.get('description'):
        issue['fields']['description'] = issue_args['description']

    if issue_args.get('components'):
        components = [{"name": comp} for comp in argToList(issue_args["components"])]
        issue['fields']['components'] = components

    if issue_args.get('security'):
        issue['fields']['security'] = {"name": issue_args['security']}

    if issue_args.get('environment'):
        issue['fields']['environment'] = issue_args['environment']

    if issue_args.get('labels') and isinstance(issue_args.get('labels'), str):
        issue['fields']['labels'] = issue_args['labels'].split(",")

    if issue_args.get('labels') and isinstance(issue_args.get('labels'), list):
        issue['fields']['labels'] = issue_args['labels']

    if issue_args.get('priority'):
        if not issue['fields'].get('priority'):
            issue['fields']['priority'] = {}
        issue['fields']['priority']['name'] = issue_args['priority']

    duedate = issue_args.get('duedate') or issue_args.get('dueDate')
    if duedate:
        issue['fields']['duedate'] = duedate

    if issue_args.get('assignee'):
        if not issue['fields'].get('assignee'):
            issue['fields']['assignee'] = {}
        issue['fields']['assignee']['name'] = issue_args['assignee']

    if issue_args.get('assignee_id'):
        if not issue['fields'].get('assignee'):
            issue['fields']['assignee'] = {}
        issue['fields']['assignee']['accountId'] = issue_args['assignee_id']

    if issue_args.get('reporter_id'):
        if not issue['fields'].get('reporter'):
            issue['fields']['reporter'] = {}
        issue['fields']['reporter']['accountId'] = issue_args['reporter_id']

    if issue_args.get('reporter'):
        if not issue['fields'].get('reporter'):
            issue['fields']['reporter'] = {}
        issue['fields']['reporter']['name'] = issue_args['reporter']

    demisto.debug(f'The issue after updating relevant fields: {issue}')
    return issue


def get_issue(issue_id, headers=None, expand_links=False, is_update=False, get_attachments=False, extra_fields=None):
    j_res = jira_req('GET', f'rest/api/latest/issue/{issue_id}', resp_type='json')
    if expand_links == "true":
        expand_urls(j_res)

    attachments = demisto.get(j_res, 'fields.attachment')  # list of all attachments
    # handle issues were we allowed incorrect values of true
    if get_attachments == "true" or get_attachments == "\"true\"":
        get_attachments = True
    else:
        get_attachments = False

    if get_attachments and attachments:
        for attachment in attachments:
            filename, attachments_zip = get_attachment_data(attachment)
            demisto.results(fileResult(filename=filename, data=attachments_zip))

    md_and_context = generate_md_context_get_issue(j_res, extra_fields=extra_fields)
    human_readable = tableToMarkdown(demisto.command(), md_and_context['md'], argToList(headers))
    if is_update:
        human_readable += f'Issue #{issue_id} was updated successfully'

    contents = j_res
    outputs = {'Ticket(val.Id == obj.Id)': md_and_context['context']}

    return human_readable, outputs, contents


def get_project_role_command(project_key, role_name):
    j_res = get_project_roles(project_key)
    url = j_res.get(role_name)
    if not url:
        return_error(f"Role '{role_name}' not found")

    role_id = url.split('/')[-1]
    url = f'rest/api/latest/project/{project_key}/role/{role_id}'
    j_res = jira_req('GET', url, resp_type='json')
    return j_res


def get_project_roles(project_key):
    url = f'rest/api/latest/project/{project_key}/role'
    j_res = jira_req('GET', url, resp_type='json')
    return j_res


def issue_query_command(query, start_at='', max_results=None, headers='', extra_fields=None):
    nofields: set = set()

    j_res = run_query(query, start_at, max_results, extra_fields, nofields)
    if not j_res:
        outputs = contents = {}
        human_readable = 'No issues matched the query.'
    else:
        issues = demisto.get(j_res, 'issues')
        md_and_context = generate_md_context_get_issue(issues, extra_fields, nofields)
        human_readable = tableToMarkdown(demisto.command(), t=md_and_context['md'], headers=argToList(headers))
        contents = j_res
        outputs = {'Ticket(val.Id == obj.Id)': md_and_context['context']}

    return human_readable, outputs, contents


def create_issue_command():
    url = 'rest/api/latest/issue'
    issue = get_issue_fields(issue_creating=True, **demisto.args())
    j_res = jira_req('POST', url, json.dumps(issue), resp_type='json')

    md_and_context = generate_md_context_create_issue(j_res, project_key=demisto.getArg('projectKey'),
                                                      project_name=demisto.getArg('projectName'))
    human_readable = tableToMarkdown(demisto.command(), md_and_context['md'], "")
    contents = j_res
    outputs = md_and_context['context']
    return_outputs(readable_output=human_readable, outputs=outputs, raw_response=contents)


def add_user_to_project_command(user_email, project_key, role_name):
    result = get_account_id_from_attribute(user_email)

    if isinstance(result, CommandResults) and isinstance(result.raw_response, dict):
        user_id = result.raw_response.get('AccountID')
    else:
        return_error(f'{result}')

    role_id = get_project_role_command(project_key, role_name).get('id')
    url = f'rest/projectconfig/latest/roles/{project_key}/{role_id}'
    json_data = {'users': [user_id], "groups": []}
    return jira_req('POST', url, json.dumps(json_data)).text


def edit_issue_command(issue_id, mirroring=False, headers=None, status=None, transition=None, resolution=None, **kwargs):
    issue = get_issue_fields(mirroring=mirroring, **kwargs)
    if status and transition:
        return_error("Please provide only status or transition, but not both.")
    elif status:
        edit_status(issue_id, status, issue, resolution)
    elif transition:
        edit_transition(issue_id, transition, issue, resolution)
    else:
        url = f'rest/api/latest/issue/{issue_id}/'
        jira_req('PUT', url, json.dumps(issue))
    return get_issue(issue_id, headers, is_update=True)


def append_to_field_command(issue_id, field_json, headers=None):
    issue = jira_req('GET', f'rest/api/latest/issue/{issue_id}', resp_type='json')
    fields = json.loads(field_json, strict=False)
    new_data = {}
    for field in fields:
        field_type = __get_field_type(field)
        if not field_type:
            raise DemistoException(f"field {field} could not be updated.")

        if field_type == 'Field Not Found':
            raise DemistoException(f'Could not identify field {field}. Make sure it was entered with correct field ID.')

        current_data_in_field = issue.get('fields', {}).get(field)
        if not current_data_in_field:
            new_data[field] = __create_value_by_type(field_type, fields[field])
        else:
            new_data[field] = __add_value_by_type(field_type, current_data_in_field, fields[field])

    _update_fields(issue_id, new_data)

    return get_issue(issue_id, headers, is_update=True)


def _update_fields(issue_id, new_data):
    url = f'rest/api/latest/issue/{issue_id}/'
    if new_data:
        jira_req('PUT', url, json.dumps({'fields': new_data}))


def get_organizations_command(project_key=None, start="0", limit="50", account_id=None):
    if project_key:
        url = f'/rest/servicedeskapi/servicedesk/{project_key}/organization'
    else:
        url = '/rest/servicedeskapi/organization'
    body = {
        'start': arg_to_number(start),
        'limit': arg_to_number(limit),
    }

    if account_id:
        body['accountId'] = account_id

    if response := jira_req('GET', url, params=body, resp_type='json').get('values'):
        [org.pop('_links') for org in response]

        hr = tableToMarkdown(name='Organizations', t=response, headers=['name', 'id', 'created'],
                             json_transform_mapping={'created': JsonTransformer(func=lambda x: x.get('friendly'))})

        return CommandResults(outputs=response, outputs_prefix='Jira.Organizations', outputs_key_field='id', readable_output=hr)

    return CommandResults(readable_output='No results found.')


def get_field_command(issue_id, field):
    fields = argToList(field)
    return get_issue(issue_id, extra_fields=fields, is_update=False)


def __get_field_type(field_id):
    fields = jira_req('GET', 'rest/api/2/field')
    field_data_filter = filter(lambda x: x.get('id') == field_id, fields.json())
    try:
        field_data = next(field_data_filter)
        return field_data.get('schema', {}).get('type')
    except StopIteration:
        return 'Field Not Found'


def __add_value_by_type(type, current_value, new_value):
    if type == 'string':
        new_val = current_value + " , " + new_value
    elif type == 'array':
        new_val = current_value + [new_value]
    else:
        raise DemistoException(f"Command only support string or array-typed fields. Field given is typed {type}")
    return new_val


def __create_value_by_type(type, value):
    if type == 'string':
        return str(value)
    elif type == 'array':
        return [value]
    else:
        raise DemistoException(f"Command only support string or array-typed fields. Field given is typed {type}")


def edit_status(issue_id, status, issue, resolution=None):
    # check for all authorized transitions available for this user
    # if the requested transition is available, execute it.
    if not issue:
        issue = {}
    j_res = list_transitions_data_for_issue(issue_id)
    # When changing the status we search the transition that leads to this status
    statuses = [transition.get('to', {}).get('name', '') for transition in j_res.get('transitions')]
    for i, transition in enumerate(statuses):
        if transition.lower() == status.lower():
            url = f'rest/api/latest/issue/{issue_id}/transitions?expand=transitions.fields'
            issue['transition'] = {"id": str(j_res.get('transitions')[i].get('id'))}
            if resolution:
                if issue.get('fields'):
                    issue['fields'].update({"resolution": {'name': resolution}})
                else:
                    issue['fields'] = {"resolution": {'name': resolution}}
            return jira_req('POST', url, json.dumps(issue))

    return_error(f'Status "{status}" not found. \nValid statuses are: {statuses} \n')
    return None


def list_transitions_data_for_issue(issue_id):
    """
    This function performs the API call for getting a list of all possible transitions for a given issue.
    :param issue_id: The ID of the issue.
    :return: API raw response.
    """
    url = f'rest/api/2/issue/{issue_id}/transitions'
    return jira_req('GET', url, resp_type='json')


def edit_transition(issue_id, transition_name, issue, resolution=None):
    """
    This function changes a transition for a given issue.
    :param issue_id: The ID of the issue.
    :param transition_name: The name of the new transition.
    :return: None
    """
    if issue is None:
        issue = {}
    j_res = list_transitions_data_for_issue(issue_id)
    transitions_data = j_res.get('transitions')
    for transition in transitions_data:
        if transition.get('name') == transition_name:
            url = f'rest/api/latest/issue/{issue_id}/transitions?expand=transitions.fields'
            issue['transition'] = {"id": transition.get("id")}
            if resolution:
                if issue.get('fields'):
                    issue['fields'].update({"resolution": {'name': resolution}})
                else:
                    issue['fields'] = {"resolution": {'name': resolution}}
            return jira_req('POST', url, json.dumps(issue))

    return_error(f'Transitions "{transition_name}" not found. \nValid transitions are: {transitions_data} \n')
    return None


def list_transitions_command(args):
    """
    This command list all possible transitions for a given issue.
    :param args: args['issueId']: The ID of the issue.
    :return: CommandResults object with the list of transitions
    """
    issue_id = args.get('issueId')
    transitions_data_list = list_transitions_data_for_issue(issue_id)
    transitions_names = [transition.get('name') for transition in transitions_data_list.get('transitions')]
    readable_output = tableToMarkdown(
        'List Transitions:', transitions_names, headers=['Transition Name']
    )
    outputs = {'ticketId': issue_id,
               'transitions': transitions_names
               }
    return CommandResults(raw_response=transitions_names, readable_output=readable_output,
                          outputs_prefix="Ticket.Transitions", outputs_key_field="ticketId", outputs=outputs)


def get_comments_command(issue_id):
    url = f'rest/api/latest/issue/{issue_id}/comment'
    body = jira_req('GET', url, resp_type='json')
    comments = []
    if body.get("comments"):
        for comment in body.get("comments"):
            comments.append({
                'Comment': comment.get("body"),
                'User': demisto.get(comment, 'updateAuthor.name'),
                'Created': comment.get("created")
            })

        human_readable = tableToMarkdown("Comments", comments)
        contents = body
        outputs = {'Ticket(val.Id == obj.Id)': {'Id': issue_id, "Comment": comments}}
        return human_readable, outputs, contents

    else:
        return 'No comments were found in the ticket', None, None


def add_comment(issue_id, comment, visibility=''):
    url = f'rest/api/latest/issue/{issue_id}/comment'
    comment = {
        "body": comment
    }
    if visibility:
        comment["visibility"] = {
            "type": "role",
            "value": visibility
        }
    return jira_req('POST', url, json.dumps(comment), resp_type='json')


def add_comment_command(issue_id, comment, visibility=''):
    data = add_comment(issue_id, comment, visibility)
    md_list = []
    if not isinstance(data, list):
        data = [data]
    for element in data:
        md_obj = {
            'id': demisto.get(element, 'id'),
            'key': demisto.get(element, 'updateAuthor.key'),
            'comment': demisto.get(element, 'body'),
            'ticket_link': demisto.get(element, 'self')
        }
        md_list.append(md_obj)

    human_readable = tableToMarkdown(demisto.command(), md_list, "")
    contents = data
    return_outputs(readable_output=human_readable, outputs={}, raw_response=contents)


def issue_upload_command(issue_id, upload, attachment_name=None):
    j_res = upload_file(upload, issue_id, attachment_name)
    md = generate_md_upload_issue(j_res, issue_id)
    human_readable = tableToMarkdown(demisto.command(), md, "")
    contents = j_res
    return_outputs(readable_output=human_readable, outputs={}, raw_response=contents)


def upload_file(entry_id, issue_id, attachment_name=None):
    file_name, file_bytes = get_file(entry_id)
    return jira_req(
        method='POST',
        resource_url=f'rest/api/latest/issue/{issue_id}/attachments',
        headers={
            'X-Atlassian-Token': 'no-check'
        },
        files={'file': (attachment_name or file_name, file_bytes)},
        resp_type='json'
    )


def get_file(entry_id):
    get_file_path_res = demisto.getFilePath(entry_id)
    file_path = get_file_path_res["path"]
    file_name = get_file_path_res["name"]
    with open(file_path, 'rb') as f:
        file_bytes = f.read()
    return file_name, file_bytes


def add_link_command(issue_id, title, url, summary=None, global_id=None, relationship=None,
                     application_type=None, application_name=None):
    req_url = f'rest/api/latest/issue/{issue_id}/remotelink'
    link = {
        "object": {
            "url": url,
            "title": title
        }
    }

    if summary:
        link['summary'] = summary
    if global_id:
        link['globalId'] = global_id
    if relationship:
        link['relationship'] = relationship
    if application_type or application_name:
        link['application'] = {}
    if application_type:
        link['application']['type'] = application_type
    if application_name:
        link['application']['name'] = application_name

    data = jira_req('POST', req_url, json.dumps(link), resp_type='json')
    md_list = []
    if not isinstance(data, list):
        data = [data]
    for element in data:
        md_obj = {
            'id': demisto.get(element, 'id'),
            'key': demisto.get(element, 'updateAuthor.key'),
            'comment': demisto.get(element, 'body'),
            'ticket_link': demisto.get(element, 'self')
        }
        md_list.append(md_obj)
    human_readable = tableToMarkdown(demisto.command(), md_list, "", removeNull=True)

    return_outputs(readable_output=human_readable, outputs={}, raw_response=data)


def delete_issue_command(issue_id_or_key):
    url = f'rest/api/latest/issue/{issue_id_or_key}'
    issue = get_issue_fields(**demisto.args())
    result = jira_req('DELETE', url, json.dumps(issue))
    if result.status_code == 204:
        demisto.results('Issue deleted successfully.')
    else:
        demisto.results('Failed to delete issue.')


def update_issue_assignee_command(issue_id, assignee=None, assignee_id=None):
    if assignee:  # for jira server
        body = {"name": assignee}
    elif assignee_id:  # for jira cloud
        body = {"accountId": assignee_id}
    else:
        raise DemistoException('Please provide assignee for Jira Server or assignee_id for Jira Cloud')
    url = f'rest/api/latest/issue/{issue_id}/assignee'
    jira_req('PUT', url, json.dumps(body))
    return get_issue(issue_id, is_update=True)


def test_module() -> str:
    """
    Performs basic get request to get item samples
    """
    user_data = jira_req('GET', 'rest/api/latest/myself', resp_type='json')
    if demisto.params().get('isFetch'):
        run_query(demisto.getParam('query'), '', max_results=1)

    if not user_data.get('active'):
        raise Exception(f'Test module for Jira failed for the configured parameters.'
                        f'please Validate that the user is active. Response: {str(user_data)}')
    outgoing_mirror = demisto.params().get('outgoing_mirror')
    if outgoing_mirror:
        try:
            custom_fields = get_custom_fields()
            if custom_fields is None:
                return_warning("Test module has finished successfully!."
                               "Please Note: There was a problem getting the list of custom fields for mirror "
                               "outgoing incidents.")
        except Exception as e:
            return_warning("Test module has finished successfully!."
                           f" Please Note: There was a problem getting the list of custom fields for mirror outgoing "
                           f"incidents.\n The error is:{e}.")

    return 'ok'


def get_entries_for_fetched_incident(ticket_id, should_get_comments, should_get_attachments):
    """
    Get entries for incident
    :param ticket_id: the remote system id of the ticket
    :param should_get_comments: if 'True', return ticket's comments
    :param should_get_attachments: if 'True', return ticket's attachments
    :return: incident's entries.
    """
    entries: dict = {'comments': [], 'attachments': []}
    try:
        _, _, raw_response = get_issue(issue_id=ticket_id)
        entries = get_incident_entries(raw_response, '', False, should_get_comments, should_get_attachments)
    except Exception as e:
        demisto.debug(f'could not get attachments for {ticket_id} while fetch this incident because: {str(e)}')
    finally:
        return entries


def fetch_incidents(query, id_offset, should_get_attachments, should_get_comments, should_mirror_in, should_mirror_out,
                    comment_tag, attachment_tag, fetch_by_created=None):
    last_run = demisto.getLastRun()
    demisto.debug(f'last_run: {last_run}' if last_run else 'last_run is empty')
    last_created_time = ''
    if last_run:
        id_offset = last_run.get('idOffset') or ''
        last_created_time = last_run.get('lastCreatedTime') or ''
    if not id_offset:
        id_offset = 0

    incidents, max_results = [], 50
    if fetch_by_created and last_created_time:
        last_issue_time = parse(last_created_time)
        assert last_issue_time is not None, f'could not parse {last_created_time}'
        minute_to_fetch = last_issue_time - timedelta(minutes=2)
        formatted_minute_to_fetch = minute_to_fetch.strftime('%Y-%m-%d %H:%M')
        query = f'{query} AND created>=\"{formatted_minute_to_fetch}\"'
    else:
        if id_offset:
            query = f'{query} AND id >= {id_offset} ORDER BY id ASC'
        if fetch_by_created:
            query = f'{query} AND created>-1m'

    res = run_query(query, '', max_results)
    if res:
        curr_id = int(id_offset)
        for ticket in res.get('issues'):
            ticket_id = int(ticket.get('id'))
            ticket_created = ticket.get('fields', {}).get('created', '')
            if ticket_id <= curr_id:
                continue
            if ticket_id > int(id_offset):
                id_offset = ticket_id
                last_created_time = ticket_created
            incidents.append(create_incident_from_ticket(ticket, should_get_attachments, should_get_comments,
                                                         should_mirror_in, should_mirror_out, comment_tag,
                                                         attachment_tag))

    demisto.setLastRun({'idOffset': id_offset, 'lastCreatedTime': last_created_time})
    return incidents


def get_attachment_data(attachment):
    """
    Get attachments content
    :param attachment: attachment metadata
    :return: attachment name and content
    """
    attachment_url = attachment.get('content')
    filename = attachment.get('filename')
    attachments_zip = jira_req(method='GET', resource_url=attachment_url, link=True).content

    return filename, attachments_zip


def get_attachments(attachments, incident_modified_date, only_new=True):
    """
    Get incident attachments as fileResults objects
    :param attachments: the issue's attachments
    :param incident_modified_date: the date the incident was last updated
    :param only_new: if 'True', getting only attachments that was added after the incident_modified_date
    :return: a list of fileResults
    """
    file_results = []
    # list of all attachments
    if attachments:
        if not only_new:
            for attachment in attachments:
                filename, attachments_zip = get_attachment_data(attachment)
                file_results.append(fileResult(filename=filename, data=attachments_zip))
        else:
            for attachment in attachments:
                attachment_modified_date: datetime = \
                    parse(dict_safe_get(attachment, ['created'], "", str))  # type: ignore
                if incident_modified_date < attachment_modified_date:
                    filename, attachments_zip = get_attachment_data(attachment)
                    file_results.append(fileResult(filename=filename, data=attachments_zip))
    return file_results


def get_comments(comments, incident_modified_date, only_new=True):
    """
    Get issue's comments
    :param comments: the issue's comments
    :param incident_modified_date: the date the incident was last updated
    :param only_new: if 'True', getting only comments that was added after the incident_modified_date
    :return: a list of comments
    """
    if not only_new:
        return comments
    else:
        returned_comments = []
        for comment in comments:
            comment_modified_date: datetime = parse(dict_safe_get(comment, ['updated'], "", str))  # type: ignore
            if incident_modified_date < comment_modified_date:
                returned_comments.append(comment)
        return returned_comments


def get_incident_entries(issue, incident_modified_date, only_new=True, should_get_comments=True,
                         should_get_attachments=True):
    """
    This function get comments and attachments from Jira Ticket, if specified, for a Jira incident.
    :param issue: the incident to get its entries
    :param incident_modified_date: when the incident was last modified
    :param only_new: if 'True' it gets only entries that were added after the incident was last modified
    :param should_get_comments: if 'True', the returned entries will contain comments
    :param should_get_attachments: if 'True' the returned entries will contain attachments
    :return: the incident's comments and attachments
    """
    entries: dict = {'comments': [], 'attachments': []}
    if should_get_comments:
        _, _, comments_content = get_comments_command(issue['id'])
        if comments_content:
            raw_comments_content = comments_content
            commands = get_comments(raw_comments_content.get('comments', []), incident_modified_date, only_new)
            entries['comments'] = commands
    if should_get_attachments:
        attachments = demisto.get(issue, 'fields.attachment')
        if attachments:
            file_results = get_attachments(attachments, incident_modified_date, only_new)
            if file_results:
                entries['attachments'] = file_results
    return entries


def get_mapping_fields_command() -> GetMappingFieldsResponse:
    """
     this command pulls the remote schema for the different incident types, and their associated incident fields,
     from the remote system.
    :return: A list of keys you want to map
    """
    jira_incident_type_scheme = SchemeTypeMapping(type_name=JIRA_INCIDENT_TYPE_NAME)
    custom_fields = get_custom_fields()
    ISSUE_INCIDENT_FIELDS.update(custom_fields)
    for argument, description in ISSUE_INCIDENT_FIELDS.items():
        jira_incident_type_scheme.add_field(name=argument, description=description)

    mapping_response = GetMappingFieldsResponse()
    mapping_response.add_scheme_type(jira_incident_type_scheme)

    return mapping_response


def handle_incoming_closing_incident(incident_data):
    """
    This function creates an object for issues with status 'Done' in order to close its incident when getting remote
     data
    :param incident_data: the data of an incident
    :return: the object using to close the incident in Demito
    """
    closing_entry: dict = {}
    if incident_data.get('fields').get('status').get('name') == 'Done':
        demisto.debug(f"Closing Jira issue {incident_data.get('id')}")
        closing_entry = {
            'Type': EntryType.NOTE,
            'Contents': {
                'dbotIncidentClose': True,
                'closeReason': JIRA_RESOLVE_REASON,
            },
            'ContentsFormat': EntryFormat.JSON
        }
    return closing_entry


def update_remote_system_command(args):
    """ Mirror-out data that is in Demito into Jira issue

    Notes:
        1. Documentation on mirroring - https://xsoar.pan.dev/docs/integrations/mirroring_integration

    Args:
        args: A dictionary contains the next data regarding a modified incident: data, entries, incident_changed,
         remote_incident_id, inc_status, delta

    Returns: The incident id that was modified.
    """
    remote_args = UpdateRemoteSystemArgs(args)
    entries = remote_args.entries
    remote_id = remote_args.remote_incident_id
    demisto.debug(
        f'Update remote system check if need to update: remoteId: {remote_id}, incidentChanged: '
        f'{remote_args.incident_changed}, data:'
        f' {remote_args.data}, entries: {entries}')
    try:
        if remote_args.delta and remote_args.incident_changed:
            demisto.debug(f'Got the following delta keys {str(list(remote_args.delta.keys()))} to update Jira '
                          f'incident {remote_id}')
            # take the val from data as it's the updated value
            delta = {k: remote_args.data.get(k) for k in remote_args.delta}
            demisto.debug(f'sending the following data to edit the issue with: {delta}')
            edit_issue_command(remote_id, mirroring=True, **delta)

        else:
            demisto.debug(f'Skipping updating remote incident fields [{remote_id}] '
                          f'as it is not new nor changed')

        if entries:
            for entry in entries:
                demisto.debug(f'Sending entry {entry.get("id")}, type: {entry.get("type")}')
                if entry.get('type') == 3:
                    demisto.debug('Add new file\n')
                    path_res = demisto.getFilePath(entry.get('id'))
                    file_name = path_res.get('name')
                    upload_file(entry.get('id'), remote_id, file_name)
                else:  # handle comments
                    demisto.debug('Add new comment\n')
                    add_comment(remote_id, str(entry.get('contents', '')))
    except Exception as e:
        demisto.error(f"Error in Jira outgoing mirror for incident {remote_args.remote_incident_id} \n"
                      f"Error message: {str(e)}")
    finally:
        return remote_id


def get_user_info_data():
    """
    This function returns details for a current user in order to get timezone.
    :return: API response
    """
    HEADERS['Accept'] = "application/json"
    return jira_req(method='GET', resource_url='rest/api/latest/myself', headers=HEADERS)


def get_modified_remote_data_command(args):
    """
    available from Cortex XSOAR version 6.1.0. This command queries for incidents that were modified since the last
    update. If the command is implemented in the integration, the get-remote-data command will only be performed on
    incidents returned from this command, rather than on all existing incidents.
    :param args: args['last_update']: Date string represents the last time we retrieved modified incidents for this
     integration.
    :return: GetModifiedRemoteDataResponse: this is the object that maintains a list of incident ids to run
     'get-remote-data' on.
    """
    remote_args = GetModifiedRemoteDataArgs(args)
    modified_issues_ids = []
    HEADERS['Accept'] = "application/json"
    try:
        res = get_user_info_data()
    except Exception as e:
        demisto.error(f'Could not get Jira\'s timezone for get-modified-remote-data. failed because: {e}')
    else:
        if res.status_code == 200:
            timezone_name = res.json().get('timeZone')
            if not timezone_name:
                demisto.error(f'Could not get Jira\'s time zone for get-modified-remote-data.Got unexpected reason:'
                              f' {res.json()}')
            last_update_date = parse(remote_args.last_update, settings={'TIMEZONE': timezone_name})
            assert last_update_date is not None, f'could not parse {remote_args.last_update}'
            last_update: str = last_update_date \
                .strftime('%Y-%m-%d %H:%M')
            demisto.debug(f'Performing get-modified-remote-data command. Last update is: {last_update}')
            _, _, context = issue_query_command(f'updated > "{last_update}"', max_results=100)
            modified_issues = context.get('issues', [])
            modified_issues_ids = [issue.get('id') for issue in modified_issues if issue.get('id')]
            demisto.debug(f'Performing get-modified-remote-data command. Issue IDs to update in XSOAR:'
                          f' {modified_issues_ids}')
        else:
            demisto.error(f'Could not get Jira\'s time zone for get-modified-remote-data. status code:'
                          f' {res.status_code}.'
                          f' reason: {res.reason}')
    finally:
        return GetModifiedRemoteDataResponse(modified_issues_ids)


def get_remote_data_command(args) -> GetRemoteDataResponse:
    """ Mirror-in data to incident from Jira into XSOAR 'jira issue' incident.

    Notes:
        1. Documentation on mirroring - https://xsoar.pan.dev/docs/integrations/mirroring_integration

    Args:
        args:
            id: Remote incident id.
            lastUpdate: Server last sync time with remote server.

    Returns:
        GetRemoteDataResponse: Structured incident response.
    """
    incident_update = {}
    parsed_entries = []
    parsed_args = GetRemoteDataArgs(args)
    try:
        # Get raw response on issue ID
        _, _, issue_raw_response = get_issue(issue_id=parsed_args.remote_incident_id)
        demisto.info('get remote data')
        # Timestamp - Issue last modified in jira server side
        jira_modified_date: datetime = \
            parse(dict_safe_get(issue_raw_response, ['fields', 'updated'], "", str))  # type: ignore
        # Timestamp - Issue last sync in demisto server side
        incident_modified_date: datetime = parse(parsed_args.last_update)  # type: ignore
        # Update incident only if issue modified in Jira server-side after the last sync
        demisto.info(f"jira_modified_date{jira_modified_date}")
        demisto.info(f"incident_modified_date{incident_modified_date}")
        if jira_modified_date > incident_modified_date:
            demisto.info('updating remote data')
            incident_update = issue_raw_response

            demisto.info(f"\nUpdate incident:\n\tIncident name: Jira issue {issue_raw_response.get('id')}\n\t"
                         f"Reason: Issue modified in remote.\n\tIncident Last update time: {incident_modified_date}"
                         f"\n\tRemote last updated time: {jira_modified_date}\n")
            demisto.info(f"\n raw incident: {issue_raw_response}\n")

            closed_issue = handle_incoming_closing_incident(incident_update)
            if closed_issue:
                demisto.info(
                    f'Close incident with ID: {parsed_args.remote_incident_id} this issue was marked as "Done"')
                incident_update['in_mirror_error'] = ''
                return GetRemoteDataResponse(incident_update, [closed_issue])

            entries = get_incident_entries(issue_raw_response, incident_modified_date)

            for comment in entries['comments']:
                parsed_entries.append({
                    'Type': EntryType.NOTE,
                    'Contents': comment.get('body', ''),
                    'ContentsFormat': EntryFormat.TEXT,
                    # 'Tags': ['comment'],  # the list of tags to add to the entry
                    'Note': True
                })
            for attachment in entries['attachments']:
                parsed_entries.append(attachment)
        if parsed_entries:
            demisto.info(f'Update the next entries: {parsed_entries}')

        incident_update['in_mirror_error'] = ''
        return GetRemoteDataResponse(incident_update, parsed_entries)

    except Exception as e:
        demisto.info(f"Error in Jira incoming mirror for incident {parsed_args.remote_incident_id} \n"
                     f"Error message: {str(e)}")

        if "Rate limit exceeded" in str(e):
            return_error("API rate limit")

        if incident_update:
            incident_update['in_mirror_error'] = str(e)
        else:
            incident_update = {
                'id': parsed_args.remote_incident_id,
                'in_mirror_error': str(e)
            }
        return GetRemoteDataResponse(
            mirrored_object=incident_update,
            entries=[]
        )


def main():
    demisto.debug(f'Command being called is {demisto.command()}')
    fetch_query = demisto.params().get('query')
    id_offset = demisto.params().get('idOffset')
    fetch_attachments = demisto.params().get('fetch_attachments')
    fetch_comments = demisto.params().get('fetch_comments')
    incoming_mirror = demisto.params().get("incoming_mirror")
    outgoing_mirror = demisto.params().get('outgoing_mirror')
    comment_tag = demisto.params().get('comment_tag')
    attachment_tag = demisto.params().get('file_tag')
    fetch_by_created = demisto.params().get('fetchByCreated')
    try:
        # Remove proxy if not set to true in params
        handle_proxy()

        if demisto.command() == 'test-module':
            # This is the call made when pressing the integration test button.
            demisto.results(test_module())

        elif demisto.command() == 'fetch-incidents':
            # Set and define the fetch incidents command to run after activated via integration settings.
            incidents = fetch_incidents(fetch_query, id_offset, fetch_attachments, fetch_comments, incoming_mirror,
                                        outgoing_mirror, comment_tag, attachment_tag, fetch_by_created)
            demisto.incidents(incidents)

        elif demisto.command() == 'jira-get-issue':
            human_readable, outputs, raw_response = get_issue(**snakify(demisto.args()))
            return_outputs(human_readable, outputs, raw_response)

        elif demisto.command() == 'jira-get-project-role':
            return_results(get_project_role_command(**demisto.args()))

        elif demisto.command() == 'jira-issue-query':
            human_readable, outputs, raw_response = issue_query_command(**snakify(demisto.args()))
            return_outputs(human_readable, outputs, raw_response)

        elif demisto.command() == 'jira-create-issue':
            create_issue_command()

        elif demisto.command() == 'jira-add-user-to-project':
            return_results(add_user_to_project_command(**demisto.args()))

        elif demisto.command() == 'jira-edit-issue':
            human_readable, outputs, raw_response = edit_issue_command(**snakify(demisto.args()))
            return_outputs(human_readable, outputs, raw_response)

        elif demisto.command() == 'jira-append-to-field':
            human_readable, outputs, raw_response = append_to_field_command(**snakify(demisto.args()))
            return_outputs(human_readable, outputs, raw_response)

        elif demisto.command() == 'jira-get-specific-field':
            human_readable, outputs, raw_response = get_field_command(**snakify(demisto.args()))
            return_outputs(human_readable, outputs, raw_response)

        elif demisto.command() == 'jira-get-comments':
            human_readable, outputs, raw_response = get_comments_command(**snakify(demisto.args()))
            return_outputs(readable_output=human_readable, outputs=outputs, raw_response=raw_response)

        elif demisto.command() == 'jira-issue-add-comment':
            add_comment_command(**snakify(demisto.args()))

        elif demisto.command() == 'jira-issue-upload-file':
            issue_upload_command(**snakify(demisto.args()))

        elif demisto.command() == 'jira-issue-add-link':
            add_link_command(**snakify(demisto.args()))

        elif demisto.command() == 'jira-delete-issue':
            delete_issue_command(**snakify(demisto.args()))

        elif demisto.command() == 'jira-get-id-offset':
            get_id_offset()

        elif demisto.command() == 'get-mapping-fields':
            return_results(get_mapping_fields_command())

        elif demisto.command() == 'update-remote-system':
            return_results(update_remote_system_command(demisto.args()))

        elif demisto.command() == 'get-remote-data':
            return_results(get_remote_data_command(demisto.args()))

        elif demisto.command() == 'jira-get-id-by-attribute':
            return_results(get_account_id_from_attribute(**demisto.args()))

        elif demisto.command() == 'jira-get-organizations':
            return_results(get_organizations_command(**snakify(demisto.args())))

        elif demisto.command() == 'jira-list-transitions':
            return_results(list_transitions_command(demisto.args()))

        elif demisto.command() == 'get-modified-remote-data':
            return_results(get_modified_remote_data_command(demisto.args()))

        elif demisto.command() == 'jira-issue-assign':
            human_readable, outputs, raw_response = update_issue_assignee_command(**snakify(demisto.args()))
            return_outputs(human_readable, outputs, raw_response)
        else:
            raise NotImplementedError(f'{COMMAND_NOT_IMPELEMENTED_MSG}: {demisto.command()}')

    except Exception as err:
        if isinstance(err, NotImplementedError) and COMMAND_NOT_IMPELEMENTED_MSG in str(err):
            raise
        return_error(str(err))

    finally:
        LOG.print_log()


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