Jira V3
Use the Jira integration to manage issues, create Cortex XSOAR incidents from Jira projects, and mirror issues to existing issue incidents in Cortex XSOAR. The integration now supports both OnPrem, and Cloud instances.
Case Management · Atlassian Jira
Details
| ID | Jira V3 |
|---|---|
| Provider | Atlassian |
| Category | Case Management |
| From Version | 6.8.0 |
| Docker Image | demisto/btfl-soup:1.0.1.10120494 |
| Supported Modules | Agentix Cortex Cloud Cloud Runtime Security Cloud Posture Security XSIAM EDR |
README
Use the Jira integration to manage issues and create Cortex XSOAR incidents from Jira projects. From Cortex XSOAR version 6.0 and above, the integration also mirrors issues to existing issue incidents in Cortex XSOAR. The integration now supports both OnPrem, and Cloud instances.
This integration was integrated and tested with: Jira Cloud Platform V3 (for Cloud), Jira Software Cloud (for Cloud), Jira Server Platform 9.6.0 (for OnPrem), Jira Software Server 9.6.0 (for OnPrem).
Some changes have been made that might affect your existing content.
If you are upgrading from a previous version of this integration, see Breaking Changes.
Configure Atlassian Jira V3 on Cortex XSOAR
- Navigate to Settings > Integrations > Servers & Services.
- Search for Atlassian Jira V3.
- Authentication: Basic Authentication, Personal Access Token, and OAuth 2.0 are used for Jira Cloud and On-prem. Read the Authentication process in order to configure your instance
-
Click Add instance to create and configure a new integration instance.
Parameter Description Required Server URL The base URL. For a Cloud instance, use the default URL as configured. For an OnPrem instance, use your respective domain. True Cloud ID This field is used to configure the Jira Cloud. User name used only for basic authentication method False API key used only for basic authentication method False Callback URL used only for OAuth 2.0 method False Client ID used only for OAuth 2.0 method False Client Secret used only for OAuth 2.0 method False Personal Access Token used only for the Personal Access Token method False Query (in JQL) for fetching incidents The field that was selected in the “Issue Field to fetch by” can’t be used. in the query. False Issue Field to fetch by This is how the field (e.g, created date) is applied to the query: created >= {created date in last run} ORDER BY created ASC False Issue index to start fetching incidents from This parameter is dismissed if “id” is not chosen in “Issue Field to Fetch by”. This will only fetch Jira issues that are part of the same project as the issue that is configured in this parameter. If this value is 0, then the fetch mechanism will automatically start the fetch from the smallest ID with respect to the fetch query. False Trust any certificate (not secure) False Use system proxy settings False Fetch incidents False Incident type False Close Mirrored XSOAR Incident When selected, marking the Jira issue as resolved will be mirrored in Cortex XSOAR, and will close the corresponding incident. Please make sure the `Resolved` field in your Jira instance is configured in order for this to work properly, if not, it will rely on if the status of the issue is changed to `Done`. False Mirroring Direction False Attachment Entry Tag to Jira Add this tag to an entry to mirror it as an attachment to Jira. False Attachment Entry Tag from Jira Add this tag to an entry to mirror it as an attachment from Jira. False Comment Entry Tag to Jira Add this tag to an entry to mirror it as a comment in Jira. False Comment Entry Tag from Jira Add this tag to an entry to mirror it as a comment from Jira. False Fetch comments Fetch comments for a Jira ticket. False Fetch attachments Fetch attachments for a Jira ticket. False Max incidents per fetch 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. Default value is 50. False Time range for initial data fetch The time range to consider for the initial data fetch in format: <number> <unit>. This parameter is only relevant when selecting to fetch by created time, or updated time. Default is 3 days. For example: 2 minutes, 2 hours, 2 days, 2 months, 2 years False - Check Authorization Flow In Cortex XSOAR in order to authenticate and test the connection.
Configure only one of the following fields:
- Cloud ID - Used for Jira Cloud instance.
- OnPrem - Leave the Cloud ID empty, and fill in the rest of the fields.
Cloud ID
- Go to your Admin page.
- Click the Products tab on the top banner and choose the appropriate site under Sites and Products on the left side bar. Your Cloud ID will appear in the URL:
https://admin.atlassian.com/s/{cloud_id}/users
Authentication
Basic Authentication
Leave the Client ID, Client Secret and Personal Access Token fields empty and fill in the following fields:
- User name - Enter your user email.
- API key - Enter the API token. To generate API token, see here
Basic Authentication permissions
Ensure that you possess the necessary permissions by navigating to Project Settings > Permissions in the project.
Locate permissions for the tasks listed below:
- Browse projects
- Create issues
- Edit issues
- Delete issues
- Transition issues
- Create attachments
- Add comments
- Link issues
Personal Access Token Authentication
Leave the User name, API key, Client ID and Client Secret empty and fill in the following fields:
- Personal Access Token - Enter the Personal Access Token you created for a user, see here
OAuth 2.0
For both instances (Cloud ID & OnPrem), it is advised to use the https://oproxy.demisto.ninja/authcode Callback URL. The OProxy URL is a client side only web page which provides an easy interface to copy the obtained auth code from the authorization response to the integration configuration in the authorization flow steps. Optionally, if you don’t want to use the OProxy URL, you can use a localhost URL on a port that is not used locally on your machine. For example: http://localhost:9004. You will then need to copy the code from the URL address bar in the response (see OAuth 2.0 authorization Flow In Cortex XSOAR).
Cloud authentication
Go to your Developer console page, and choose the App you want to integrate with your instance. It must be of type OAuth 2.0. For creating a new app with type OAuth 2.0, click Create and choose OAuth 2.0 integration and follow the steps.
Callback URL
- Go to the Authorization tab, and click Add on the authorization with type of OAuth 2.0 (3LO).
- Insert a Callback URL.
Client ID, Client Secret
- Go to the Settings tab.
- Copy the Client ID and Secret to the Client ID and Client Secret fields, respectively.
Cloud Scopes
The integration uses the offline_access scope, in order to retrieve refresh tokens.
Classic Scopes
- read:jira-work
- read:jira-user
- write:jira-work
Granular Scopes
- read:jql:jira
- read:issue-details:jira
- write:board-scope:jira-software
- read:board-scope:jira-software
- read:sprint:jira-software
- read:epic:jira-software
- write:sprint:jira-software
OnPrem authentication
- Log in to Jira as a user with
Jira Administratorpermissions. - Click the Jira Administration tab (the gear icon found in the top right corner) and click Applications.
- To create a new
Application link, which will be used to integrate Cortex XSOAR with Jira:
a. Click Application links underIntegrations, found on the left side menu.
b. Click Create link and choose External application with the Incoming direction. - Fill in the required details as explained in the page, by inserting a Callback URL, and choosing the permission Write.
- Once the link is created, you will be able to see
Client ID, and theClient secret, which are required in the configuration screen. Copy these values and paste them into the respective fields in the configuration screen.
OnPrem Scopes
Write
OAuth 2.0 authorization Flow In Cortex XSOAR
- Create the authentication application as explained in the OAuth 2.0 section.
- Run the command
!jira-oauth-start, where you will be presented with a URL to authenticate yourself. - After authenticating, you will be redirected to the configured Callback URL, where you will retrieve the authorization code provided as a query parameter called
code. - Insert the retrieved authorization code as an argument to the
!jira-oauth-completecommand. - Run the
!jira-oauth-testto test the connection of the instance.
Authenticating using custom callback URL

Authenticating using the oproxy callback URL

Fetch Incidents
When you enable incidents fetching, you have the option to configure a query which will be used to fetch Jira issues as incidents in Cortex XSOAR. The query uses one of the following fields in order to progressively fetch incidents:
idof Jira issuescreated timeof Jira issuesupdated timeof Jira issues (using this field may cause duplicates, so be wary)
If created time, or updated time is selected when fetching incidents for the first time, then the Time range for initial data fetch argument is used in the fetch query.
By default, 50 issues are fetched for each call.
If Fetch comments is enabled, The fetched incidents will include the comments in the Jira issue, and preview them in the form of a table in the incident info tab.
If Fetch attachments is enabled, The fetched incidents will include the attachments in the Jira issue.
Note: Changing certain incidents fetching configuration parameters, such as Issue Field to fetch by on the fly may result in unintended side effects and behaviors, including duplicate or missing incidents. It is recommended to reset the “last run” timestamp before applying such changes or, preferably, configure a new integration instance with the new fetch configuration.
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.
Notes:
- For OnPrem, all the commands use the WRITE scope, for Cloud, the required scopes for each command will be mentioned under the name of the command.
- For the commands that are shared between Jira V2 and V3, we still support the arguments from V2, but are configured to be hidden, and are prioritized when supplied to the commands, to not break backwards compatibility.
jira-issue-get-attachment
Download attachments for a specific issue (Will download a file to War Room).
Scope: read:jira-work
Base Command
jira-issue-get-attachment
Input
| Argument Name | Description | Required |
|---|---|---|
| attachment_id | A CSV list of the attachments’ ID, which can be retrieved using the jira-get-issue command. |
Required |
Context Output
| Path | Type | Description |
|---|---|---|
| InfoFile.Name | String | The file name. |
| InfoFile.EntryID | String | The EntryID of the file in the War Room. |
| InfoFile.Size | Number | The size of the file (in bytes). |
| InfoFile.Type | String | The file type, as determined by libmagic (same as displayed in file entries). |
| InfoFile.Extension | String | The file extension. |
| InfoFile.Info | String | Basic information of the file. |
Command example
!jira-issue-get-attachment attachment_id=16477
Context Example
{
"InfoFile": {
"EntryID": "10906@33e912c0-76a9-4a08-8cc6-1b20b1980b20",
"Extension": "pdf",
"Info": "application/pdf",
"Name": "dummy.pdf",
"Size": 13264,
"Type": "PDF document, version 1.4, 1 pages (zip deflate encoded)"
}
}
Human Readable Output
jira-delete-issue
Deletes an issue in Jira. Issue’s sub-tasks will also be deleted if there are any.
Scope: write:jira-work
Base Command
jira-delete-issue
Input
| Argument Name | Description | Required |
|---|---|---|
| issueIdOrKey | Deprecated. Please use issue_id or issue_key. | Optional |
| issue_id | The issue ID (Issue ID or key is required). | Optional |
| issue_key | The issue key (Issue ID or key is required). | Optional |
Context Output
There is no context output for this command.
Command example
!jira-delete-issue issue_key=PROJECTKEY-114
Human Readable Output
Issue deleted successfully.
jira-get-specific-field
Gets specific fields from a Jira issue and adds it to context dynamically.
Scope: read:jira-work
Base Command
jira-get-specific-field
Input
| Argument Name | Description | Required |
|---|---|---|
| issue_id | The issue ID (Issue ID or key is required). | Optional |
| issue_key | The issue key (Issue ID or key is required). | Optional |
| issueId | Deprecated. Please use issue_id or issue_key. | Optional |
| field | Deprecated. Please use fields. | Optional |
| fields | The fields to retrieve from the issue. For example field=”customfield_164,labels”. | 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-get-specific-field issue_key=PROJECTKEY-35 fields="watches,rank"
Context Example
{
"Ticket": {
"Assignee": "Example User(example@example.com)",
"Attachments": [
{
"created": "2023-05-04T21:11:51.286+0300",
"filename": "dummy_attachment_content.txt",
"id": "16475",
"size": 13264
},
{
"created": "2023-05-04T21:32:57.042+0300",
"filename": "dummy.pdf",
"id": "16478",
"size": 13264
}
],
"Components": [
"dummy-comp",
"Integration"
],
"Created": "2023-03-01T11:34:49.730+0200",
"Creator": "Example User(example@example.com)",
"Description": "Edited subbscription",
"DueDate": "2023-01-01",
"Id": "21487",
"Key": "PROJECTKEY-35",
"Labels": [
"label1",
"label2"
],
"LastSeen": "2023-05-08T19:02:34.151+0300",
"LastUpdate": "2023-05-08T19:07:44.900+0300",
"Priority": "Highest",
"ProjectName": "Company Snoozing App",
"Status": "Backlog",
"Summary": "Edited Summary",
"customfield_10019": {
"issueFieldDisplayName": "Rank",
"rawData": "0|i00kjb:"
},
"watches": {
"issueFieldDisplayName": "Watchers",
"rawData": {
"isWatching": true,
"self": "https://api.atlassian.com/ex/jira/1234/rest/api/3/issue/PROJECTKEY-35/watchers",
"watchCount": 1
}
}
}
}
Human Readable Output
Issue PROJECTKEY-35
Assignee Created Creator Description Due Date Id Issue Type Key Labels Priority Project Name Reporter Status Summary Ticket Link Example User(example@example.com) 2023-03-01T11:34:49.730+0200 Example User(example@example.com) Edited subbscription 2023-01-01 21487 Story PROJECTKEY-35 label1,
label2Highest Company Snoozing App Example User(example@example.com) Backlog Edited Summary https://api.atlassian.com/ex/jira/1234/rest/api/3/issue/21487
jira-issue-add-link
Creates (or updates) an issue link.
Scope: write:jira-work
Base Command
jira-issue-add-link
Input
| Argument Name | Description | Required |
|---|---|---|
| globalId | Deprecated. Please use global_id. | Optional |
| global_id | If the global ID is provided and a remote issue link exists with that global ID, the remote issue link is updated. | Optional |
| relationship | The object relationship to the issue, for example: causes. | Optional |
| url | The URL link. | Required |
| title | The link title. | Required |
| summary | The link summary. | Optional |
| issueId | Deprecated. Please use issue_id or issue_key. | Optional |
| issue_id | The issue ID (Issue ID or key is required). | Optional |
| issue_key | The issue key (Issue ID or key is required). | Optional |
| application_type | The application type of the linked remote application. For example “com.atlassian.confluence”. | Optional |
| applicationType | Deprecated. Please use application_type. | Optional |
| application_name | The application name of the linked remote application. For example “My Confluence Instance”. | Optional |
| applicationName | Deprecated. Please use application_name. | Optional |
Context Output
There is no context output for this command.
Command example
!jira-issue-add-link title="Demo" url="https://demisto.com" issue_key=PROJECTKEY-35
Human Readable Output
Remote Issue Link
id ticket_link 16506 https://api.atlassian.com/ex/jira/1234/rest/api/3/issue/PROJECTKEY-35/remotelink/16506
jira-get-id-by-attribute
Gets the account ID for a given user attribute.
Scope: read:jira-user
Base Command
jira-get-id-by-attribute
Input
| Argument Name | Description | Required |
|---|---|---|
| attribute | The user attribute value. For Jira Cloud, this value can be the display name, or email address, and for Jira OnPrem, this value can be the display name, email address, or username. | Required |
| max_results | 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 greater than this number, your search results are truncated. Default is 50. | 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=Tomer
Context Example
{
"Jira": {
"User": {
"AccountId": "12345",
"Attribute": "Tomer"
}
}
}
Human Readable Output
The account ID that holds the attribute
Tomer: 12345
jira-issue-query
Queries Jira issues.
Scope: read:jira-work
Base Command
jira-issue-query
Input
| Argument Name | Description | Required |
|---|---|---|
| query | The JQL query string. | Required |
| start_at | The index (integer) of the first issue to return (0-based). | Optional |
| startAt | Deprecated. Please use start_at. | Optional |
| max_results | 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 greater than this number, your search results are truncated. | Optional |
| maxResults | Deprecated. Please use max_results. | Optional |
| headers | Displays the headers in human readable format. | Optional |
| fields | A CSV list of fields to return (Using the name or ID of the fields) to context data. Supplying all will return all the issue fields (which will include nested values), except for the comments, which can be retrieved using the !jira-get-comments command. |
Optional |
| extraFields | Deprecated. Please use fields. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Jira.Query.nextPageToken | String | A token for fetching the next page of the query if such exists. |
| 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.Description | String | The ticket’s description. |
| Ticket.RawDescription | String | The ticket’s raw description. |
| Ticket.Labels | Array | The ticket’s labels. |
| Ticket.Components | Array | The ticket’s components. |
| Ticket.Status | String | The ticket status. |
| Ticket.Priority | String | The ticket priority. |
| 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" max_results=2 fields="watches,rank"
Context Example
{
"Ticket": [
{
"Assignee": "",
"Attachments": [
{
"created": "2023-03-26T18:06:24.325+0300",
"filename": "DemistoContent-InstallContentEnvironment-160822-0023-68.pdf",
"id": "16426",
"size": 17815
}
],
"Components": [],
"Created": "2023-03-01T11:59:18.202+0200",
"Creator": "Example User(example@example.com)",
"Description": "Dummy description",
"DueDate": "",
"Id": "21490",
"Key": "XSOAR-19",
"Labels": [],
"LastSeen": "",
"LastUpdate": "2023-05-04T21:41:44.516+0300",
"Priority": "Highest",
"ProjectName": "XSOARJiraV3",
"Status": "To Do",
"Summary": "something",
"customfield_10019": {
"issueFieldDisplayName": "Rank",
"rawData": "0|i00kjr:"
},
"watches": {
"issueFieldDisplayName": "Watchers",
"rawData": {
"isWatching": true,
"self": "https://api.atlassian.com/ex/jira/1234/rest/api/3/issue/XSOAR-19/watchers",
"watchCount": 1
}
}
},
{
"Assignee": "",
"Attachments": [],
"Components": [],
"Created": "2023-03-01T14:10:55.495+0200",
"Creator": "Example User(example@example.com)",
"Description": "Dummy",
"DueDate": "",
"Id": "21494",
"Key": "XSOAR-18",
"Labels": [],
"LastSeen": "",
"LastUpdate": "2023-03-01T14:10:55.495+0200",
"Priority": "Medium",
"ProjectName": "XSOARJiraV3",
"Status": "To Do",
"Summary": "something something",
"customfield_10019": {
"issueFieldDisplayName": "Rank",
"rawData": "0|i00kkn:"
},
"watches": {
"issueFieldDisplayName": "Watchers",
"rawData": {
"isWatching": true,
"self": "https://api.atlassian.com/ex/jira/1234/rest/api/3/issue/XSOAR-18/watchers",
"watchCount": 1
}
}
}
]
}
Human Readable Output
Issue XSOAR-19
Assignee Created Creator Description Due Date Id Issue Type Key Labels Priority Project Name Reporter Status Summary Ticket Link 2023-03-01T11:59:18.202+0200 Example User(example@example.com) Dummy description 21490 Task XSOAR-19 Highest XSOARJiraV3 Example User(example@example.com) To Do something https://api.atlassian.com/ex/jira/1234/rest/api/3/issue/21490
Issue XSOAR-18
Assignee Created Creator Description Due Date Id Issue Type Key Labels Priority Project Name Reporter Status Summary Ticket Link 2023-03-01T14:10:55.495+0200 Example User(example@example.com) Dummy 21494 Task XSOAR-18 Medium XSOARJiraV3 Example User(example@example.com) To Do something something https://api.atlassian.com/ex/jira/1234/rest/api/3/issue/21494
jira-get-id-offset
Returns the ID offset, which is the ID of the first issue that was created in the current Jira instance, or the ID of the first issue with respect to the given query argument if given. You can use its result as the initial ID when fetching incidents by the issue id field.
Scope: read:jira-work
Base Command
jira-get-id-offset
Input
| Argument Name | Description | Required |
|---|---|---|
| query | The query that will be used to retrieve the first issue ID in it. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Ticket.idOffSet | String | The ID offset. |
Command example
!jira-get-id-offset query="status!=done"
Context Example
{
"Ticket": {
"idOffSet": "10161"
}
}
Human Readable Output
ID Offset: 10161
jira-issue-add-comment
Adds a new comment to an existing Jira issue.
Scope: write:jira-work
Base Command
jira-issue-add-comment
Input
| Argument Name | Description | Required |
|---|---|---|
| issueId | Deprecated. Please use issue_id or issue_key. | Optional |
| issue_id | The issue ID (Issue ID or key is required). | Optional |
| issue_key | The issue key (Issue ID or key is required). | Optional |
| 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 issue_key=PROJECTKEY-31 comment="New comment"
Human Readable Output
Comment added successfully
Comment Id Ticket Link New comment 18456 https://api.atlassian.com/ex/jira/1234/rest/api/3/issue/21432/comment/18456
jira-issue-upload-file
Uploads a file attachment to an issue.
Scope: write:jira-work
Base Command
jira-issue-upload-file
Input
| Argument Name | Description | Required |
|---|---|---|
| issueId | Deprecated. Please use issue_id or issue_key. | Optional |
| issue_id | The issue ID (Issue ID or key is required). | Optional |
| issue_key | The issue key (Issue ID or key is required). | Optional |
| upload | The entry ID to upload. | Required |
| attachment_name | The attachment name to be displayed in Jira (overrides the original file name). | Optional |
| attachmentName | Deprecated. Please use attachment_name. | Optional |
Context Output
There is no context output for this command.
Command example
!jira-issue-upload-file upload=10833@33e912c0-76a9-4a08-8cc6-1b20b1980b20 issue_key=PROJECTKEY-31
Human Readable Output
Attachment added successfully
Attachment Link Attachment Name Id Issue Key https://api.atlassian.com/ex/jira/1234/rest/api/3/attachment/16505 dummy.pdf 16505 PROJECTKEY-31
jira-issue-delete-file
Delete an attachment from an issue.
Scope: write:jira-work
Base Command
jira-issue-delete-file
Input
| Argument Name | Description | Required |
|---|---|---|
| attachment_id | The attachment ID. | Required |
Context Output
There is no context output for this command.
Command example
!jira-issue-delete-file attachment_id=ATTACHMENT_ID
Human Readable Output
Attachment id ATTACHMENT_ID was deleted successfully.
jira-list-transitions
Lists all possible transitions for a given ticket in its current status.
Scope: read:jira-work
Base Command
jira-list-transitions
Input
| Argument Name | Description | Required |
|---|---|---|
| issueId | Deprecated. Please use issue_id or issue_key. | Optional |
| issue_id | The issue ID (Issue ID or key is required). | Optional |
| issue_key | The issue key (Issue ID or key is required). | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Ticket.Id | String | The ticket ID. |
| Ticket.Key | String | The ticket key. |
| Ticket.Transitions | Object | An object that holds data about all possible transitions. |
| Ticket.Transitions.transitions | Array | An array of all possible transitions. |
| Ticket.Transitions.ticketId | String | The ticket ID. |
Command example
!jira-list-transitions issue_key=PROJECTKEY-35
Context Example
{
"Ticket": {
"Key": "PROJECTKEY-35",
"Transitions": {
"ticketId": "PROJECTKEY-35",
"transitions": [
"Backlog",
"In Development"
]
}
}
}
Human Readable Output
List Transitions
Transition Names Backlog In Development
jira-edit-issue
Modifies an issue in Jira. If the status or transition fields are given, then the other supplied fields should appear on the transition screen.
Scope: write:jira-work
Base Command
jira-edit-issue
Input
| Argument Name | Description | Required |
|---|---|---|
| parent_issue_key | The parent issue key (if you’re editing a sub-task). This argument is only relevant for Jira Cloud. | Optional |
| parent_issue_id | The parent issue ID (if you’re editing a sub-task). This argument is only relevant for Jira Cloud. | Optional |
| action | Whether to append or rewrite the values. Only issue fields of type string or array support appending. If the field is of type string, then the new appended value will be: ‘old value, new value’, where they will be separated by a comma. Possible values are: append, rewrite. Default is rewrite. | Optional |
| issueId | Deprecated. Please use issue_id or issue_key. | Optional |
| issue_id | The issue ID (Issue ID or key is required). | Optional |
| issue_key | The issue key (Issue ID or key is required). | Optional |
| issue_json | The issue object (in JSON format). This argument is for advanced users, as when utilizing this argument, one cannot use other arguments (it will raise an error) except the issue_id issue_eky action transition and status and must input all required arguments into the issue_json. In addition, when issue_json is used, it is not possible to use the project name but only the project key. For example {“fields”:{“customfield_10037”:”field_value”, “summary”: “some summary”, “project”: {“key”: “"}}}. For more information about this argument, see https://developer.atlassian.com/server/jira/platform/jira-rest-api-example-create-issue-7897248/. |
Optional |
| issueJson | Deprecated. Please use issue_json. | Optional |
| summary | The issue summary. | Optional |
| description | The issue description. | Optional |
| labels | A CSV list of labels. | Optional |
| priority | The issue priority, for example: High, Medium. | Optional |
| due_date | The due date for the issue (in the format yyyy-mm-dd). | Optional |
| dueDate | Deprecated. Please use due_date. | Optional |
| assignee | The name of the assignee. Relevant for Jira Server only. If you are using Jira Cloud, provide the assignee_id argument instead. Use the jira-get-id-by-attribute command to get the user’s name. | Optional |
| assignee_id | The account ID of the assignee. Relevant for Jira Cloud only. Use the jira-get-id-by-attribute command to get the user’s account ID. | Optional |
| status | The issue status (Either choose the issue status or transition, but not both). | Optional |
| transition | The issue transition (Either choose the issue status or transition, but not both). | 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 | A CSV list of components. For example components=”component1,component2”. When using a Jira OnPrem instance, or using the action append, the entered components must already exist. | 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.Description | String | The ticket’s project description. |
| Ticket.RawDescription | String | The ticket’s project raw description. |
| Ticket.Labels | Array | The ticket’s project labels. |
| Ticket.Status | String | The ticket status. |
| Ticket.Priority | String | The ticket priority. |
| 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-edit-issue action=rewrite issue_key=PROJECTKEY-35 assignee_id=12345 description="Edited subbscription" due_date="2023-01-01" environment="Windows XP" labels="label1,label2" priority="Highest" security=Anyone status="Backlog" summary="Edited Summary"
Context Example
{
"Ticket": {
"Assignee": "Example User(example@example.com)",
"Attachments": [
{
"created": "2023-05-07T20:00:59.121+0300",
"filename": "dummy_attachment_content.txt",
"id": "16504",
"size": 13264
},
{
"created": "2023-05-04T21:29:49.246+0300",
"filename": "dummy.pdf",
"id": "16477",
"size": 13264
}
],
"Components": [
"dummy-comp",
"Integration"
],
"Created": "2023-03-01T11:34:49.730+0200",
"Creator": "Example User(example@example.com)",
"Description": "Edited subbscription",
"DueDate": "2023-01-01",
"Id": "21487",
"Key": "PROJECTKEY-35",
"Labels": [
"label1",
"label2"
],
"LastSeen": "2023-05-08T19:02:34.151+0300",
"LastUpdate": "2023-05-08T19:07:44.900+0300",
"Priority": "Highest",
"ProjectName": "Company Snoozing App",
"Status": "Backlog",
"Summary": "Edited Summary"
}
}
Human Readable Output
Issue PROJECTKEY-35
Assignee Created Creator Description Due Date Id Issue Type Key Labels Priority Project Name Reporter Status Summary Ticket Link Example User(example@example.com) 2023-03-01T11:34:49.730+0200 Example User(example@example.com) Edited subbscription 2023-01-01 21487 Story PROJECTKEY-35 label1,
label2Highest Company Snoozing App Example User(example@example.com) Backlog Edited Summary https://api.atlassian.com/ex/jira/1234/rest/api/3/issue/21487
jira-issue-assign
Assigns an assignee to an existing issue.
Scope: write:jira-work
Base Command
jira-issue-assign
Input
| Argument Name | Description | Required |
|---|---|---|
| issue_id | The issue ID (Issue ID or key is required). | Optional |
| issue_key | The issue key (Issue ID or key is required). | Optional |
| assignee | The name of the assignee. Relevant for Jira Server only. If you are using Jira Cloud, provide the assignee_id argument instead. Use the jira-get-id-by-attribute command to get the user’s name. | Optional |
| assignee_id | The account ID of the assignee. Relevant for Jira Cloud only. 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.Description | String | The ticket’s project description. |
| Ticket.RawDescription | String | The ticket’s project raw description. |
| Ticket.Labels | Array | The ticket’s project labels. |
| Ticket.Status | String | The ticket status. |
| Ticket.Priority | String | The ticket priority. |
| 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-assign issue_key=PROJECTKEY-35 assignee_id=12345
Context Example
{
"Ticket": {
"Assignee": "Example User(example@example.com)",
"Attachments": [
{
"created": "2023-05-07T20:00:59.121+0300",
"filename": "dummy_attachment_content.txt",
"id": "16504",
"size": 13264
},
{
"created": "2023-05-04T21:29:49.246+0300",
"filename": "dummy.pdf",
"id": "16477",
"size": 13264
}
],
"Components": [
"dummy-comp",
"Integration"
],
"Created": "2023-03-01T11:34:49.730+0200",
"Creator": "Example User(example@example.com)",
"Description": "Edited subbscription",
"DueDate": "2023-01-01",
"Id": "21487",
"Key": "PROJECTKEY-35",
"Labels": [
"label1",
"label2"
],
"LastSeen": "2023-05-08T19:02:34.151+0300",
"LastUpdate": "2023-05-08T19:07:44.900+0300",
"Priority": "Highest",
"ProjectName": "Company Snoozing App",
"Status": "Backlog",
"Summary": "Edited Summary"
}
}
Human Readable Output
Issue PROJECTKEY-35
Assignee Created Creator Description Due Date Id Issue Type Key Labels Priority Project Name Reporter Status Summary Ticket Link Example User(example@example.com) 2023-03-01T11:34:49.730+0200 Example User(example@example.com) Edited subbscription 2023-01-01 21487 Story PROJECTKEY-35 label1,
label2Highest Company Snoozing App Example User(example@example.com) Backlog Edited Summary https://api.atlassian.com/ex/jira/1234/rest/api/3/issue/21487
jira-create-issue
Creates a new issue in Jira.
Scope: write:jira-work
Base Command
jira-create-issue
Input
| Argument Name | Description | Required |
|---|---|---|
| issue_json | ‘The issue object (in JSON format). This argument is for advanced users, as when utilizing this argument, one cannot use other arguments (it will raise an error) and must input all required arguments into the issue_json. In addition, when issue_json is used, it is not possible to use the project name but only the project key. For example {“fields”:{“customfield_10037”:”field_value”, “summary”: “some summary”, “project”: {“key”: “"}}}. For more information about this argument, see https://developer.atlassian.com/server/jira/platform/jira-rest-api-example-create-issue-7897248/'. | Optional |
| issueJson | Deprecated. Please use issue_json. | Optional |
| summary | The summary of the issue (Summary value is required, either from the “summary” argument, or from the “issue_json” argument). | Optional |
| project_key | The project key with which to associate the issue (Project Key or name is required). | Optional |
| projectKey | Deprecated. Please use project_key. | Optional |
| issue_type_name | Selects an issue type by name, for example: “Problem”. (Issue type name or id is required). | Optional |
| issueTypeName | Deprecated. Please use issue_type_name. | Optional |
| issue_type_id | Selects an issue type by its numeric ID (Issue type name or id is required). | Optional |
| issueTypeId | Deprecated. Please use issue_type_id. | Optional |
| project_name | The project name with which to associate the issue (Project Key or name is required). | Optional |
| projectName | Deprecated. Please use project_name. | 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 |
| due_date | The due date for the issue (in the format yyyy-mm-dd). | Optional |
| dueDate | Deprecated. Please use due_date. | Optional |
| assignee | The name of the assignee. Relevant for Jira Server only. If you are using Jira Cloud, provide the assignee_id argument instead. Use the jira-get-id-by-attribute command to get the user’s name. | Optional |
| assignee_id | The account ID of the assignee. Relevant for Jira Cloud only. Use the jira-get-id-by-attribute command to get the user’s account ID. | Optional |
| reporter_id | The account ID of the reporter. Relevant for Jira Cloud only. Use the jira-get-id-by-attribute command to get the user’s account ID. | Optional |
| reporter | The name of the of the reporter. Relevant for Jira Server only. Please use reporter_id for Jira Cloud. Use the jira-get-id-by-attribute command to get the user’s name. | Optional |
| parent_issue_key | The parent issue key (if you’re creating a sub-task). | Optional |
| parentIssueKey | Deprecated. Please use parent_issue_key. | Optional |
| parent_issue_id | The parent issue ID (if you’re creating a sub-task). | Optional |
| parentIssueId | Deprecated. Please use parent_issue_id. | 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 of the issue. For example security=”Anyone”. | Optional |
| components | The component names of the issue. For example components=”component1,component2”. When using a Jira OnPrem instance, the entered components must already exist. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Ticket.Id | String | The ticket ID. |
| Ticket.Key | String | The ticket key. |
Command example
!jira-create-issue summary="Dummy Summary" assignee_id=12345 components="dummy-comp,New-Component" description="Dummy description" due_date="2023-01-01" environment="Windows XP" issue_type_name="Sub-task" labels="label1,label2" parent_issue_key=PROJECTKEY-35 priority=Highest project_name="Company Snoozing App" reporter_id=12345 security=Assignee
Context Example
{
"Ticket": {
"Id": "21619",
"Key": "PROJECTKEY-145"
}
}
Human Readable Output
Issue PROJECTKEY-145
Id Key Project Key Ticket Link 21619 PROJECTKEY-145 PROJECTKEY https://api.atlassian.com/ex/jira/1234/rest/api/3/issue/21619
jira-get-issue
Fetches an issue from Jira.
Scope: read:jira-work
Base Command
jira-get-issue
Input
| Argument Name | Description | Required |
|---|---|---|
| fields | A CSV list of fields to return (Using the name or ID of the fields) to context data. Supplying all will return all the issue fields (which will include nested values), except for the comments, which can be retrieved using the !jira-get-comments command. |
Optional |
| issueId | Deprecated. Please use issue_id or issue_key. | Optional |
| issue_id | The issue ID (Issue ID or key is required). | Optional |
| issue_key | The issue key (Issue ID or key is required). | Optional |
| headers | Displays the headers in human readable format. | Optional |
| get_attachments | If “true”, retrieves the issue attachments and downloads the file to the War Room. Possible values are: true, false. Default is false. | Optional |
| getAttachments | Deprecated. Please use get_attachments. Possible values are: true, false. | Optional |
| expand_links | If “true”, expands the issue links (the linked issues and subtasks). Possible values are: true, false. Default is false. | Optional |
| expandLinks | Deprecated. Please use expand_links. Possible values are: true, false. | Optional |
Note: When supplying the all value to the fields argument, then all the issue fields will be returned, and every issue field that was not parsed by the code, will be parsed in the following format: {customfield_1: SOME_NESTED_VALUE} -> {customfield_1: {issueFieldDisplayName: THE_DISPLAY_NAME_OF_THE_FIELD, rawData: SOME_NESTED_VALUE}}
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, in the form `Username(User email address)`. |
| Ticket.Creator | String | The user who created the ticket. |
| Ticket.Summary | String | The ticket summary. |
| Ticket.Description | String | The ticket’s project description. |
| Ticket.RawDescription | String | The ticket’s project raw description. |
| Ticket.Status | String | The ticket status. |
| Ticket.Labels | Array | The ticket’s labels. |
| Ticket.Components | Array | The ticket’s components. |
| Ticket.Priority | String | The ticket priority. |
| 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. |
| Ticket.Attachments.created | Date | The created time of the attachment. |
| Ticket.Attachments.filename | String | The file name of the attachment. |
| Ticket.Attachments.id | String | The id of the attachment. |
| Ticket.Attachments.size | Number | The size (in bytes) of the attachment. |
| InfoFile.Name | String | The file name. |
| InfoFile.EntryID | String | The ID for locating the file in the War Room. |
| InfoFile.Size | Number | The size of the file (in bytes). |
| InfoFile.Type | String | The file type, as determined by libmagic (same as displayed in file entries). |
| InfoFile.Extension | String | The file extension. |
| InfoFile.Info | String | Basic information about the file. |
Command example
!jira-get-issue issue_key=PROJECTKEY-35 expand_links=true fields=watches,rank get_attachments=true
Context Example
{
"InfoFile": [
{
"EntryID": "10845@33e912c0-76a9-4a08-8cc6-1b20b1980b20",
"Extension": "png",
"Info": "image/png",
"Name": "1682340338180_home_screen_background_mirrored_from_xsoar.png",
"Size": 212597,
"Type": "PNG image data, 430 x 932, 8-bit/color RGBA, non-interlaced"
},
{
"EntryID": "10846@33e912c0-76a9-4a08-8cc6-1b20b1980b20",
"Extension": "txt",
"Info": "text/plain; charset=utf-8",
"Name": "dummy_attachment_content.txt",
"Size": 13264,
"Type": "PDF document, version 1.4, 1 pages (zip deflate encoded)"
}
],
"Ticket": [
{
"Assignee": "Example User(example@example.com)",
"Attachments": [
{
"created": "2023-04-24T15:46:18.919+0300",
"filename": "1682340338180_home_screen_background_mirrored_from_xsoar.png",
"id": "16466",
"size": 212597
},
{
"created": "2023-05-07T20:00:59.121+0300",
"filename": "dummy_attachment_content.txt",
"id": "16504",
"size": 13264
}
],
"Components": [
"dummy-comp",
"Integration"
],
"Created": "2023-03-01T11:34:49.730+0200",
"Creator": "Example User(example@example.com)",
"Description": "Edited subbscription",
"DueDate": "2023-01-01",
"Id": "21487",
"Key": "PROJECTKEY-35",
"Labels": [
"label1",
"label2"
],
"LastSeen": "2023-05-08T19:02:34.151+0300",
"LastUpdate": "2023-05-08T19:05:03.569+0300",
"Priority": "Highest",
"ProjectName": "Company Snoozing App",
"Status": "Selected for Development",
"Summary": "Edited Summary",
"customfield_10019": {
"issueFieldDisplayName": "Rank",
"rawData": "0|i00kjb:"
},
"watches": {
"issueFieldDisplayName": "Watchers",
"rawData": {
"isWatching": true,
"self": "https://api.atlassian.com/ex/jira/1234/rest/api/3/issue/PROJECTKEY-35/watchers",
"watchCount": 1
}
}
},
{
"Assignee": "",
"Attachments": [
{
"created": "2023-03-26T18:06:24.325+0300",
"filename": "DemistoContent-InstallContentEnvironment-160822-0023-68.pdf",
"id": "16426",
"size": 17815
}
],
"Components": [],
"Created": "2023-03-01T11:59:18.202+0200",
"Creator": "Example User(example@example.com)",
"Description": "Dummy description",
"DueDate": "",
"Id": "21490",
"Key": "XSOAR-19",
"Labels": [],
"LastSeen": "",
"LastUpdate": "2023-05-04T21:41:44.516+0300",
"Priority": "Highest",
"ProjectName": "XSOARJiraV3",
"Status": "To Do",
"Summary": "something",
"customfield_10019": {
"issueFieldDisplayName": "Rank",
"rawData": "0|i00kjr:"
},
"watches": {
"issueFieldDisplayName": "Watchers",
"rawData": {
"isWatching": true,
"self": "https://api.atlassian.com/ex/jira/1234/rest/api/3/issue/XSOAR-19/watchers",
"watchCount": 1
}
}
}
]
}
Human Readable Output
Issue COMPANYSA-35
Assignee Created Creator Description Due Date Id Issue Type Key Labels Priority Project Name Reporter Status Summary Ticket Link Example User(example@example.com) 2023-03-01T11:34:49.730+0200 Example User(example@example.com) hi 2023-05-24 21487 Story COMPANYSA-35 label1,
label2Highest Company Snoozing App Example User(example@example.com) In Progress XSOAR Meeting, Ammended summary https://api.atlassian.com/ex/jira/15d0e445-bd38-483c-b3fd-d15f562f369b/rest/api/3/issue/21487
Issue XSOAR-19
Assignee Created Creator Description Due Date Id Issue Type Key Labels Priority Project Name Reporter Status Summary Ticket Link 2023-03-01T11:59:18.202+0200 Example User(example@example.com) Dummy description 21490 Task XSOAR-19 Highest XSOARJiraV3 Example User(example@example.com) To Do something https://api.atlassian.com/ex/jira/1234/rest/api/3/issue/21490
jira-get-comments
Returns the comments added to a ticket.
Scope: read:jira-work
Base Command
jira-get-comments
Input
| Argument Name | Description | Required |
|---|---|---|
| limit | The maximum number of results to return. Default is 50 and maximum is 5000. Default is 50. | Optional |
| issueId | Deprecated. Please use issue_id or issue_key. | Optional |
| issue_id | The issue ID (Issue ID or key is required). | Optional |
| issue_key | The issue key (Issue ID or key is required). | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Ticket.Id | String | The ID of the ticket. |
| Ticket.Key | String | The ticket key. |
| Ticket.Comment.Id | String | The ID of the comment. |
| Ticket.Comment.Comment | String | The text of the comment. |
| Ticket.Comment.Created | Date | The comment creation date. |
| Ticket.Comment.Updated | Date | The comment updated date. |
| Ticket.Comment.User | String | The user that created the comment. |
| Ticket.Comment.UpdateUser | String | The user that updated the comment. |
Command example
!jira-get-comments issue_key=PROJECTKEY-35 limit=2
Context Example
{
"Ticket": {
"Comment": [
{
"Comment": "New comment",
"Created": "2023-05-04T21:29:51.036+0300",
"Id": "18423",
"UpdateUser": "Example User",
"Updated": "2023-05-04T21:29:51.036+0300",
"User": "Example User"
},
{
"Comment": "Dummy Link",
"Created": "2023-05-04T21:30:29.599+0300",
"Id": "18424",
"UpdateUser": "Example User",
"Updated": "2023-05-04T21:30:29.599+0300",
"User": "Example User"
}
],
"Id": "21487",
"Key": "PROJECTKEY-35"
}
}
Human Readable Output
Comments
Comment Created Id UpdateUser Updated User New comment 2023-05-04T21:29:51.036+0300 18423 Example User 2023-05-04T21:29:51.036+0300 Example User Dummy Link 2023-05-04T21:30:29.599+0300 18424 Example User 2023-05-04T21:30:29.599+0300 Example User
jira-oauth-complete
Use this command to complete the authorization process. After copying the authorization code found in the query parameter code of the callback URL, paste the value in the command as an argument to finish the process.
Base Command
jira-oauth-complete
Input
| Argument Name | Description | Required |
|---|---|---|
| code | The authorization code retrieved from the callback URL according to the documentation. | Required |
Context Output
There is no context output for this command.
Human Readable Output
Logged in successfully
A refresh token was saved to the integration context. This token will be
used to generate a new access token once the current one expires.
jira-oauth-start
Use this command to start the authorization process. In order to authorize the instance, first run the command, and complete the process in the URL that is returned. You will then be redirected to the callback URL where you will copy the authorization code found in the query parameter code, and paste that value in the command !jira-ouath-complete as an argument to finish the process.
Base Command
jira-oauth-start
Input
There are no input arguments for this command.
Context Output
There is no context output for this command.
Human Readable Output
In order to retrieve the authorization code, use the following link:
https://example.com
After authorizing, you will be redirected to the configured callback URL, where you will retrieve the authorization code provided as a query parameter calledcode, and insert it as an argument to the !jira-oauth-complete command
jira-oauth-test
Use this command to test the connectivity of the Jira instance.
Base Command
jira-oauth-test
Input
There are no input arguments for this command.
Context Output
There is no context output for this command.
Human Readable Output
Successful connection.
jira-issue-delete-comment
Delete a comment from an issue.
Scope: write:jira-work
Base Command
jira-issue-delete-comment
Input
| Argument Name | Description | Required |
|---|---|---|
| comment_id | The comment ID. | Required |
| issue_id | The issue ID (Issue ID or key is required). | Optional |
| issue_key | The issue key (Issue ID or key is required). | Optional |
Context Output
There is no context output for this command.
Command example
!jira-issue-delete-comment issue_key=PROJECTKEY-35 comment_id=18423
Human Readable Output
Comment deleted successfully.
jira-issue-edit-comment
Edit a comment that is part of an issue.
Scope: write:jira-work
Base Command
jira-issue-edit-comment
Input
| Argument Name | Description | Required |
|---|---|---|
| issue_id | The issue ID (Issue ID or key is required). | Optional |
| issue_key | The issue key (Issue ID or key is required). | Optional |
| comment_id | The comment ID. | Required |
| comment | The content of the comment to modify. | Required |
| visibility | The roles that can view the comment, for example: Administrators. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Ticket.Id | String | The ID of the ticket. |
| Ticket.Comment.Id | String | The ID of the comment. |
| 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-issue-edit-comment issue_key=PROJECTKEY-35 comment_id=18425 comment="New Hello There"
Context Example
{
"Ticket": {
"Comment": [
{
"Comment": "Dummy Link",
"Created": "2023-05-04T21:30:29.599+0300",
"Id": "18424",
"UpdateUser": "Example User",
"Updated": "2023-05-04T21:30:29.599+0300",
"User": "Example User"
},
{
"Comment": "New Hello There",
"Created": "2023-05-04T21:32:58.870+0300",
"Id": "18425",
"UpdateUser": "Example User",
"Updated": "2023-05-08T19:08:04.346+0300",
"User": "Example User"
},
{
"Comment": "Dummy Link",
"Created": "2023-05-04T21:33:29.986+0300",
"Id": "18426",
"UpdateUser": "Example User",
"Updated": "2023-05-04T21:33:29.986+0300",
"User": "Example User"
}
],
"Id": "21487",
"Key": "PROJECTKEY-35"
}
}
Human Readable Output
The comment has been edited successfully
jira-issue-list-fields
This command returns the issue fields, both system and custom fields.
Scope: read:jira-work
Base Command
jira-issue-list-fields
Input
| Argument Name | Description | Required |
|---|---|---|
| limit | The maximum number of results to return. This argument will be ignored if page or page_size arguments were given. Default value is 50. | Optional |
| page | The page number. Default value is 0. | Optional |
| page_size | The number of requested results per page. Default value is 50. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Jira.IssueField.id | String | The ID of the field. |
| Jira.IssueField.key | String | The key of the field. |
| Jira.IssueField.name | String | The name of the field. |
| Jira.IssueField.custom | String | Whether the field is a custom field. |
| Jira.IssueField.orderable | String | Whether the content of the field can be used to order lists. |
| Jira.IssueField.navigable | String | Whether the field can be used as a column on the issue navigator. |
| Jira.IssueField.searchable | String | Whether the content of the field can be searched. |
| Jira.IssueField.clauseNames | Array | The names that can be used to reference the field in an advanced search. |
| Jira.IssueField.scope | Object | The scope of the field. |
| Jira.IssueField.scope.type | String | The type of scope. Valid values: PROJECT, TEMPLATE |
| Jira.IssueField.scope.project | Object | The project the item has scope in. |
| Jira.IssueField.scope.project.self | String | The URL of the project details. |
| Jira.IssueField.scope.project.id | String | The ID of the project. |
| Jira.IssueField.scope.project.key | String | The key of the project. |
| Jira.IssueField.scope.project.name | String | The name of the project. |
| Jira.IssueField.scope.project.projectTypeKey | String | The project type of the project. Valid values: software, service_desk, business |
| Jira.IssueField.scope.project.simplified | Boolean | Whether or not the project is simplified. |
| Jira.IssueField.scope.project.avatarUrls | Object | The URLs of the project’s avatars. |
| Jira.IssueField.scope.project.projectCategory | Object | The category the project belongs to. |
| Jira.IssueField.scope.project.projectCategory.self | String | The URL of the project category. |
| Jira.IssueField.scope.project.projectCategory.id | String | The ID of the project category. |
| Jira.IssueField.scope.project.projectCategory.description | String | The name of the project category. |
| Jira.IssueField.scope.project.projectCategory.name | String | The description of the project category. |
| Jira.IssueField.scope.type | String | The type of scope. Valid values: PROJECT, TEMPLATE |
| Jira.IssueField.schema | Object | The data schema for the field. |
| Jira.IssueField.schema.type | String | The data type of the field. |
| Jira.IssueField.schema.items | String | When the data type is an array, the name of the field items within the array. |
| Jira.IssueField.schema.system | String | If the field is a system field, the name of the field. |
| Jira.IssueField.schema.custom | String | If the field is a custom field, the URI of the field. |
| Jira.IssueField.schema.customId | Number | If the field is a custom field, the custom ID of the field. |
| Jira.IssueField.schema.configuration | Object | If the field is a custom field, the configuration of the field. |
Command example
!jira-issue-list-fields page=1 page_size=2
Context Example
{
"Jira": {
"IssueField": [
{
"clauseNames": [],
"custom": true,
"id": "customfield_10070",
"key": "customfield_10070",
"name": "Responders",
"navigable": true,
"orderable": true,
"schema": {
"custom": "com.atlassian.jira.modules.servicemanagement.responders-entity:responders-entity-field-cftype",
"customId": 10070,
"items": "string",
"type": "array"
},
"searchable": true,
"untranslatedName": "Responders"
},
{
"clauseNames": [
"cf[10071]",
"Time to resolution"
],
"custom": true,
"id": "customfield_10071",
"key": "customfield_10071",
"name": "Time to resolution",
"navigable": true,
"orderable": true,
"schema": {
"custom": "com.atlassian.servicedesk:sd-sla-field",
"customId": 10071,
"type": "sd-servicelevelagreement"
},
"searchable": true,
"untranslatedName": "Time to resolution"
}
]
}
}
Human Readable Output
Issue Fields
Custom Id Name Schema Type Searchable true customfield_10070 Responders array true true customfield_10071 Time to resolution sd-servicelevelagreement true
jira-issue-to-backlog
Moves issues to backlog. If the issues supplied are NOT part of a board that supports sprints, then the board_id argument is required. At most 50 issues may be moved at once.
Scope: write:board-scope:jira-software
Base Command
jira-issue-to-backlog
Input
| Argument Name | Description | Required |
|---|---|---|
| issues | The issues to move to backlog. | Required |
| board_id | The board ID. Can only be used with a Jira Cloud instance. Run the command jira-board-list to retrieve the boards’ IDs. |
Optional |
| rank_before_issue | To rank the issues before the stated issue (supports issue key and ID). This argument can be used when supplying the board ID. | Optional |
| rank_after_issue | To rank the issues after the stated issue (supports issue key and ID). This argument can be used when supplying the board ID. | Optional |
Context Output
There is no context output for this command.
Command example
!jira-issue-to-backlog issues="TSTPRD-3,TSTPRD-6" rank_before_issue="TSTPRD-4" board_id=12
Human Readable Output
Issues were moved to Backlog successfully
jira-issue-to-board
Moves issues from backlog to board, the command does not work with boards with sprints. At most 50 issues may be moved at once. This command is only supported for Jira Cloud instances.
Scope: write:board-scope:jira-software
Base Command
jira-issue-to-board
Input
| Argument Name | Description | Required |
|---|---|---|
| issues | The issues to move to board. | Required |
| board_id | The board ID. | Required |
| rank_before_issue | To rank the issues before the stated issue (supports issue key and ID). | Optional |
| rank_after_issue | To rank the issues after the stated issue (supports issue key and ID). | Optional |
Context Output
There is no context output for this command.
Command example
!jira-issue-to-board issues="PROJECTKEY-35,PROJECTKEY-39" board_id=14
Human Readable Output
Issues were moved to Board successfully
jira-board-list
Get board data. If board_id is given, then only the board that corresponds to the board_id is returned, otherwise, the the command will return boards that match the project_key_id, type, or board_name arguments.
Scopes: read:board-scope:jira-software, read:issue-details:jira
Base Command
jira-board-list
Input
| Argument Name | Description | Required |
|---|---|---|
| board_name | The name of the board. | Optional |
| board_id | The board ID. | Optional |
| project_key_id | The project key or ID. | Optional |
| type | The type of the board. Allowed values are: scrum, kanban, simple. Possible values are: scrum, kanban, simple. | Optional |
| limit | The maximum number of results to return. This argument will be ignored if page or page_size arguments were given. Default value is 50. | Optional |
| page | The page number. Default value is 0. | Optional |
| page_size | The number of requested results per page. Default value is 50. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Jira.Board.id | Number | The ID of the board. |
| Jira.Board.self | String | The URL of the board. |
| Jira.Board.name | String | The name of the board. |
| Jira.Board.type | String | The type the board. |
| Jira.Board.admins | Object | The administrators of the board. |
| Jira.Board.admins.users | Object | The users who own the board. |
| Jira.Board.admins.users.self | String | The URL of the user. |
| Jira.Board.admins.users.displayName | String | The display name of the user. Depending on the user’s privacy setting, this may return an alternative value. |
| Jira.Board.admins.users.active | String | Whether the user is active. |
| Jira.Board.admins.users.accountId | String | The account ID of the user, which uniquely identifies the user across all Atlassian products. |
| Jira.Board.admins.users.avatarUrls | Object | The avatars of the user. |
| Jira.Board.admins.groups | Object | The groups who own the board. |
| Jira.Board.admins.groups.name | String | The name of the group. |
| Jira.Board.admins.groups.self | String | The URL of the group. |
| Jira.Board.location | Object | The container that the board is located in. |
| Jira.Board.location.projectId | Number | The project ID. |
| Jira.Board.location.userId | Number | The user ID. |
| Jira.Board.location.userAccountId | String | The user account ID. |
| Jira.Board.location.displayName | String | The display name of the project. |
| Jira.Board.location.projectName | String | The project name. |
| Jira.Board.location.projectKey | String | The project key. |
| Jira.Board.location.projectTypeKey | String | The type of the project. |
| Jira.Board.location.avatarURI | String | The avatar of the project. |
| Jira.Board.location.name | String | The name of the project. |
| Jira.Board.canEdit | String | Whether the board can be edited. |
| Jira.Board.isPrivate | String | Whether the board is private. |
| Jira.Board.favourite | String | Whether the board is selected as a favorite. |
Command example
!jira-board-list project_key_id=PROJECTKEY type=kanban
Context Example
{
"Jira": {
"Board": {
"id": 14,
"location": {
"avatarURI": "https://api.atlassian.com/ex/jira/1234/rest/api/2/universal_avatar/view/type/project/avatar/10402?size=small",
"displayName": "Company Snoozing App (PROJECTKEY)",
"name": "Company Snoozing App (PROJECTKEY)",
"projectId": 10022,
"projectKey": "PROJECTKEY",
"projectName": "Company Snoozing App",
"projectTypeKey": "software"
},
"name": "PROJECTKEY board",
"self": "https://api.atlassian.com/ex/jira/1234/rest/agile/1.0/board/14",
"type": "kanban"
}
}
}
Human Readable Output
Boards
ID Name Project ID Project Name Type 14 PROJECTKEY board 10022 Company Snoozing App kanban
jira-board-backlog-list
Get issues from the backlog of a specific board. For Jira OnPrem, the board must be of type scrum.
Scopes: read:board-scope:jira-software, read:issue-details:jira
Base Command
jira-board-backlog-list
Input
| Argument Name | Description | Required |
|---|---|---|
| board_id | The board ID. | Required |
| jql_query | A JQL query to filter the issues. | Optional |
| limit | The maximum number of results to return. This argument will be ignored if page or page_size arguments were given. Default value is 50. | Optional |
| page | The page number. Default value is 0. | Optional |
| page_size | The number of requested results per page. Default value is 50. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Jira.BoardBacklog.boardId | Number | The ID of the board. |
| Jira.BoardBacklog.Ticket.Id | Number | The ticket ID. |
| Jira.BoardBacklog.Ticket.Key | String | The ticket key. |
| Jira.BoardBacklog.Ticket.Assignee | String | The user assigned to the ticket. |
| Jira.BoardBacklog.Ticket.Creator | String | The user who created the ticket. |
| Jira.BoardBacklog.Ticket.Summary | String | The ticket summary. |
| Jira.BoardBacklog.Ticket.Status | String | The ticket status. |
| Jira.BoardBacklog.Ticket.Priority | String | The ticket priority. |
| Jira.BoardBacklog.Ticket.ProjectName | String | The ticket project name. |
| Jira.BoardBacklog.Ticket.DueDate | Date | The due date. |
| Jira.BoardBacklog.Ticket.Created | Date | The time the ticket was created. |
| Jira.BoardBacklog.Ticket.LastSeen | Date | The last time the ticket was viewed. |
| Jira.BoardBacklog.Ticket.LastUpdate | Date | The last time the ticket was updated. |
Command example
!jira-board-backlog-list board_id=14 jql_query="status!=done" limit=2
Context Example
{
"Jira": {
"BoardBacklog": {
"Ticket": [
{
"Assignee": "",
"Attachments": [],
"Components": [],
"Created": "2023-01-01T22:57:28.689+0200",
"Creator": "Example User(example@example.com)",
"Description": "",
"DueDate": "",
"Id": "21360",
"Key": "PROJECTKEY-1",
"Labels": [],
"LastSeen": "",
"LastUpdate": "2023-03-07T17:31:48.332+0200",
"Priority": "Medium",
"ProjectName": "Company Snoozing App",
"Status": "Backlog",
"Summary": "Start creating the company snoozing app"
},
{
"Assignee": "Example User()",
"Attachments": [],
"Components": [
"Integration"
],
"Created": "2023-01-30T16:42:47.476+0200",
"Creator": "Example User(example@example.com)",
"Description": "Complete this issue",
"DueDate": "2023-09-09",
"Id": "21424",
"Key": "PROJECTKEY-30",
"Labels": [
"collab",
"good_job"
],
"LastSeen": "",
"LastUpdate": "2023-03-13T21:09:47.898+0200",
"Priority": "Highest",
"ProjectName": "Company Snoozing App",
"Status": "Backlog",
"Summary": "Issue for demo"
}
],
"boardId": "14"
}
}
}
Human Readable Output
Backlog Issues
Assignee Created Creator Description DueDate Id IssueType Key Labels Priority ProjectName Reporter Status Summary TicketLink 2023-01-01T22:57:28.689+0200 Example User(example@example.com) 21360 Epic PROJECTKEY-1 Medium Company Snoozing App Example User(example@example.com) Backlog Start creating the company snoozing app https://api.atlassian.com/ex/jira/1234/rest/agile/1.0/issue/21360 Example User() 2023-01-30T16:42:47.476+0200 Example User(example@example.com) Complete this issue 2023-09-09 21424 Epic PROJECTKEY-30 collab,
good_jobHighest Company Snoozing App Example User() Backlog Issue for demo https://api.atlassian.com/ex/jira/1234/rest/agile/1.0/issue/21424
jira-board-issue-list
Get all issues from a specific board.
Scopes: read:board-scope:jira-software, read:issue-details:jira
Base Command
jira-board-issue-list
Input
| Argument Name | Description | Required |
|---|---|---|
| board_id | The board ID. | Required |
| jql_query | A JQL query to filter the issues. | Optional |
| limit | The maximum number of results to return. This argument will be ignored if page or page_size arguments were given. Default value is 50. | Optional |
| page | The page number. Default value is 0. | Optional |
| page_size | The number of requested results per page. Default value is 50. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Jira.BoardIssue.boardId | Number | The ID of the board. |
| Jira.BoardIssue.Ticket.Id | Number | The ticket ID. |
| Jira.BoardIssue.Ticket.Key | String | The ticket key. |
| Jira.BoardIssue.Ticket.Assignee | String | The user assigned to the ticket. |
| Jira.BoardIssue.Ticket.Creator | String | The user who created the ticket. |
| Jira.BoardIssue.Ticket.Summary | String | The ticket summary. |
| Jira.BoardIssue.Ticket.Status | String | The ticket status. |
| Jira.BoardIssue.Ticket.Priority | String | The ticket priority. |
| Jira.BoardIssue.Ticket.ProjectName | String | The ticket project name. |
| Jira.BoardIssue.Ticket.DueDate | Date | The due date. |
| Jira.BoardIssue.Ticket.Created | Date | The time the ticket was created. |
| Jira.BoardIssue.Ticket.LastSeen | Date | The last time the ticket was viewed. |
| Jira.BoardIssue.Ticket.LastUpdate | Date | The last time the ticket was updated. |
Command example
!jira-board-issue-list board_id=14 jql_query="status!=done" limit=2
Context Example
{
"Jira": {
"BoardIssue": {
"Ticket": [
{
"Assignee": "Example User()",
"Attachments": [],
"Components": [
"comp2",
"Integration",
"test_test"
],
"Created": "2023-01-30T16:35:50.482+0200",
"Creator": "Example User(example@example.com)",
"Description": "",
"DueDate": "2023-09-09",
"Id": "21423",
"Key": "PROJECTKEY-29",
"Labels": [
"Demisto",
"XSOAR",
"testnow"
],
"LastSeen": "2023-05-07T17:40:56.751+0300",
"LastUpdate": "2023-03-26T18:26:06.762+0300",
"Priority": "Low",
"ProjectName": "Company Snoozing App",
"Status": "Selected for Development",
"Summary": "Issue for demo"
},
{
"Assignee": "",
"Attachments": [
{
"created": "2023-01-22T16:43:29.878+0200",
"filename": "Screen Shot 2022-10-19 at 20.09.46.png",
"id": "16387",
"size": 450738
},
{
"created": "2023-02-05T21:11:43.429+0200",
"filename": "stam.png",
"id": "16399",
"size": 450738
}
],
"Components": [],
"Created": "2023-01-03T10:25:17.257+0200",
"Creator": "Example User(example@example.com)",
"Description": "This issue is for creating the home page.",
"DueDate": "",
"Id": "21361",
"Key": "PROJECTKEY-2",
"Labels": [
"Demisto",
"testnow"
],
"LastSeen": "",
"LastUpdate": "2023-02-26T18:24:04.172+0200",
"Priority": "Medium",
"ProjectName": "Company Snoozing App",
"Status": "Selected for Development",
"Summary": "Create Home Page (including the log in option)"
}
],
"boardId": "14"
}
}
}
Human Readable Output
Board Issues
Assignee Created Creator Description DueDate Id IssueType Key Labels Priority ProjectName Reporter Status Summary TicketLink Example User() 2023-01-30T16:35:50.482+0200 Example User(example@example.com) 2023-09-09 21423 Sub-task PROJECTKEY-29 Demisto,
XSOAR,
testnowLow Company Snoozing App Example User() Selected for Development Issue for demo https://api.atlassian.com/ex/jira/1234/rest/agile/1.0/issue/21423 2023-01-03T10:25:17.257+0200 Example User(example@example.com) This issue is for creating the home page. 21361 Story PROJECTKEY-2 Demisto,
testnowMedium Company Snoozing App Example User(example@example.com) Selected for Development Create Home Page (including the log in option) https://api.atlassian.com/ex/jira/1234/rest/agile/1.0/issue/21361
jira-board-sprint-list
Get all sprints of a specific board.
Scope: read:sprint:jira-software
Base Command
jira-board-sprint-list
Input
| Argument Name | Description | Required |
|---|---|---|
| board_id | The board ID. | Required |
| limit | The maximum number of results to return. This argument will be ignored if page or page_size arguments were given. Default value is 50. | Optional |
| page | The page number. Default value is 0. | Optional |
| page_size | The number of requested results per page. Default value is 50. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Jira.BoardSprint.boardId | String | The ID of the board. |
| Jira.BoardSprint.Sprints | Object | The sprints in the board. |
| Jira.BoardSprint.Sprints.id | Number | The ID of the sprint. |
| Jira.BoardSprint.Sprints.self | String | The URL of the sprint. |
| Jira.BoardSprint.Sprints.state | String | The state of the sprint. |
| Jira.BoardSprint.Sprints.name | String | The name of the sprint. |
| Jira.BoardSprint.Sprints.startDate | String | The starting date of the sprint. |
| Jira.BoardSprint.Sprints.endDate | String | The ending date of the sprint. |
| Jira.BoardSprint.Sprints.completeDate | String | The date the sprint was completed. |
| Jira.BoardSprint.Sprints.originBoardId | Number | The ID of the origin board. |
| Jira.BoardSprint.Sprints.goal | String | The goal of the sprint. |
Command example
!jira-board-sprint-list board_id=12
Context Example
{
"Jira": {
"BoardSprint": {
"Sprints": [
{
"endDate": "2023-02-27T15:12:00.000Z",
"goal": "",
"id": 4,
"name": "TSTPRD Sprint 1",
"originBoardId": 12,
"self": "https://api.atlassian.com/ex/jira/1234/rest/agile/1.0/sprint/4",
"startDate": "2023-02-13T15:12:39.565Z",
"state": "active"
},
{
"endDate": "2023-02-23T14:57:00.000Z",
"goal": "Goal",
"id": 5,
"name": "TSTPRD Sprint 2",
"originBoardId": 12,
"self": "https://api.atlassian.com/ex/jira/1234/rest/agile/1.0/sprint/5",
"startDate": "2023-02-07T14:57:00.000Z",
"state": "future"
},
{
"id": 6,
"name": "TSTPRD Sprint 3",
"originBoardId": 12,
"self": "https://api.atlassian.com/ex/jira/1234/rest/agile/1.0/sprint/6",
"state": "future"
}
],
"boardId": "12"
}
}
}
Human Readable Output
Sprints
End Date ID Name Start Date State 2023-02-27T15:12:00.000Z 4 TSTPRD Sprint 1 2023-02-13T15:12:39.565Z active 2023-02-23T14:57:00.000Z 5 TSTPRD Sprint 2 2023-02-07T14:57:00.000Z future 6 TSTPRD Sprint 3 future
jira-board-epic-list
Get all epics from a specific board
Scope: read:epic:jira-software
Base Command
jira-board-epic-list
Input
| Argument Name | Description | Required |
|---|---|---|
| board_id | The board ID. | Required |
| done | Filters results to epics that are either done or not done. This uses the Epic Status field, and not the Status field of the issue. Possible values are: false, true. Default is false. |
Optional |
| limit | The maximum number of results to return. This argument will be ignored if page or page_size arguments were given. Default value is 50. | Optional |
| page | The page number. Default value is 0. | Optional |
| page_size | The number of requested results per page. Default value is 50. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Jira.BoardEpic.boardId | Number | The ID of the board. |
| Jira.BoardEpic.Epics | Object | Holds the data of the epic issues. |
| Jira.BoardEpic.Epics.id | Number | The ID of the epic. |
| Jira.BoardEpic.Epics.key | String | The key of the epic. |
| Jira.BoardEpic.Epics.self | String | The URL of the epic. |
| Jira.BoardEpic.Epics.name | String | The name of the epic. |
| Jira.BoardEpic.Epics.summary | String | The summary of the epic. |
| Jira.BoardEpic.Epics.done | Boolean | Whether the epic has been completed or not. |
| Jira.BoardEpic.Epics.color | Object | Data about the color of the epic. |
| Jira.BoardEpic.Epics.color.key | String | The key of the color associated with the epic. |
Command example
!jira-board-epic-list board_id=14
Context Example
{
"Jira": {
"BoardEpic": {
"Epics": [
{
"color": {
"key": "color_4"
},
"done": false,
"id": 21360,
"key": "PROJECTKEY-1",
"name": "Create Company Snoozing App",
"self": "https://api.atlassian.com/ex/jira/1234/rest/agile/1.0/epic/21360",
"summary": "Start creating the company snoozing app"
},
{
"color": {
"key": "color_3"
},
"done": false,
"id": 21424,
"key": "PROJECTKEY-30",
"name": "New Epic",
"self": "https://api.atlassian.com/ex/jira/1234/rest/agile/1.0/epic/21424",
"summary": "Issue for demo"
}
],
"boardId": "14"
}
}
}
Human Readable Output
Epics
Done ID Key Name Summary false 21360 PROJECTKEY-1 Create Company Snoozing App Start creating the company snoozing app false 21424 PROJECTKEY-30 New Epic Issue for demo
jira-sprint-issue-list
Get all issues in a sprint.
Scopes: read:sprint:jira-software, read:issue-details:jira, read:jql:jira
Base Command
jira-sprint-issue-list
Input
| Argument Name | Description | Required |
|---|---|---|
| board_id | The board ID. | Optional |
| sprint_id | The sprint ID. | Required |
| jql_query | A JQL query to filter the issues. | Optional |
| limit | The maximum number of results to return. This argument will be ignored if page or page_size arguments were given. Default value is 50. | Optional |
| page | The page number. Default value is 0. | Optional |
| page_size | The number of requested results per page. Default value is 50. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Jira.SprintIssues.boardId | String | The ID of the board that the sprint belongs to. |
| Jira.SprintIssues.sprintId | String | The ID of the sprint. |
| Jira.SprintIssues.Ticket.Id | Number | The ticket ID. |
| Jira.SprintIssues.Ticket.Key | String | The ticket key. |
| Jira.SprintIssues.Ticket.Assignee | String | The user assigned to the ticket. |
| Jira.SprintIssues.Ticket.Creator | String | The user who created the ticket. |
| Jira.SprintIssues.Ticket.Summary | String | The ticket summary. |
| Jira.SprintIssues.Ticket.Status | String | The ticket status. |
| Jira.SprintIssues.Ticket.Priority | String | The ticket priority. |
| Jira.SprintIssues.Ticket.ProjectName | String | The ticket project name. |
| Jira.SprintIssues.Ticket.DueDate | Date | The due date. |
| Jira.SprintIssues.Ticket.Created | Date | The time the ticket was created. |
| Jira.SprintIssues.Ticket.LastSeen | Date | The last time the ticket was viewed. |
| Jira.SprintIssues.Ticket.LastUpdate | Date | The last time the ticket was updated. |
Command example
!jira-sprint-issue-list sprint_id=4
Context Example
{
"Jira": {
"SprintIssues": {
"Ticket": [
{
"Assignee": "",
"Attachments": [],
"Components": [],
"Created": "2023-02-02T17:59:21.527+0200",
"Creator": "Example User(example@example.com)",
"Description": "Please finish this ASAP.",
"DueDate": "",
"Id": "21430",
"Key": "TSTPRD-5",
"Labels": [],
"LastSeen": "2023-05-07T19:11:24.324+0300",
"LastUpdate": "2023-05-07T19:11:25.682+0300",
"Priority": "Medium",
"ProjectName": "TestProd",
"Status": "To Do",
"Summary": "Hire managers DO NOT DELETE OR MOVE"
},
{
"Assignee": "",
"Attachments": [],
"Components": [],
"Created": "2023-02-02T16:44:38.839+0200",
"Creator": "Example User(example@example.com)",
"Description": "",
"DueDate": "",
"Id": "21427",
"Key": "TSTPRD-2",
"Labels": [],
"LastSeen": "2023-05-07T19:37:42.323+0300",
"LastUpdate": "2023-05-07T19:11:33.460+0300",
"Priority": "Medium",
"ProjectName": "TestProd",
"Status": "To Do",
"Summary": "Build UI DO NOT DELETE OR MOVE"
},
{
"Assignee": "",
"Attachments": [],
"Components": [],
"Created": "2023-02-02T16:45:37.346+0200",
"Creator": "Example User(example@example.com)",
"Description": "",
"DueDate": "",
"Id": "21429",
"Key": "TSTPRD-4",
"Labels": [],
"LastSeen": "2023-05-04T20:45:40.938+0300",
"LastUpdate": "2023-05-04T21:03:30.876+0300",
"Priority": "Medium",
"ProjectName": "TestProd",
"Status": "To Do",
"Summary": "Build testing env"
}
],
"boardId": "12",
"sprintId": "4"
}
}
}
Human Readable Output
Sprint Issues in board 12
Assignee Created Creator Description DueDate Id IssueType Key Labels Priority ProjectName Reporter Status Summary TicketLink 2023-02-02T17:59:21.527+0200 Example User(example@example.com) Please finish this ASAP. 21430 Story TSTPRD-5 Medium TestProd Example User(example@example.com) To Do Hire managers DO NOT DELETE OR MOVE https://api.atlassian.com/ex/jira/1234/rest/agile/1.0/issue/21430 2023-02-02T16:44:38.839+0200 Example User(example@example.com) 21427 Task TSTPRD-2 Medium TestProd Example User(example@example.com) To Do Build UI DO NOT DELETE OR MOVE https://api.atlassian.com/ex/jira/1234/rest/agile/1.0/issue/21427 2023-02-02T16:45:37.346+0200 Example User(example@example.com) 21429 Task TSTPRD-4 Medium TestProd Example User(example@example.com) To Do Build testing env https://api.atlassian.com/ex/jira/1234/rest/agile/1.0/issue/21429
jira-sprint-issue-move
Moves issues to a sprint, for a given sprint ID. Issues can only be moved to open or active sprints. At most 50 issues may be moved at once.
Scope: write:sprint:jira-software
Base Command
jira-sprint-issue-move
Input
| Argument Name | Description | Required |
|---|---|---|
| issues | The issues to move to the sprint. | Required |
| sprint_id | The sprint ID. | Required |
| rank_before_issue | To rank the issues before the stated issue (supports issue key and ID). | Optional |
| rank_after_issue | To rank the issues after the stated issue (supports issue key and ID). | Optional |
Context Output
There is no context output for this command.
Command example
!jira-sprint-issue-move issues=TSTPRD-4 sprint_id=4
Human Readable Output
Issues were moved to the Sprint successfully
jira-user-group-search
Gets users and groups that match a query string.
Scope: read:jira-work
Base Command
jira-user-group-search
Input
| Argument Name | Description | Required |
|---|---|---|
| query | The search string to match against users and groups. For users, performs a case-insensitive substring match on display name and email address. For groups, performs a case-sensitive substring match on group name. Note: if a user has hidden their email address, an exact email match is required. | Required |
| limit | The maximum number of results to return. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Jira.UsersAndGroups.Users | Object | The Users that match the query string. |
| Jira.UsersAndGroups.Groups | Object | The Groups that match the query string. |
Command example
!jira-user-group-search query="query"
Context Example
{
"groups": {
"groups": [
{
"groupId": "276f955c-63d7-42c8-9520-92d01dca0625",
"html": "<b>j</b>dog-developers",
"name": "jdog-developers"
},
{
"groupId": "6e87dc72-4f1f-421f-9382-2fee8b652487",
"html": "<b>j</b>uvenal-bot",
"name": "juvenal-bot"
}
],
"header": "Showing 20 of 25 matching groups",
"total": 25
},
"users": {
"header": "Showing 20 of 25 matching users",
"total": 25,
"users": [
{
"accountId": "5b10a2844c20165700ede21g",
"accountType": "atlassian",
"avatarUrl": "https://avatar-management--avatars.server-location.prod.public.atl-paas.net/initials/MK-5.png?size=16&s=16",
"displayName": "Mia Krystof",
"html": "<strong>Mi</strong>a Krystof - <strong>mi</strong>a@example.com (<strong>mi</strong>a)",
"key": "mia",
"name": "mia"
}
]
}
}
Human Readable Output
Users
Account ID Display Name Account Type 5b10a2844c20165700ede21g Mia Krystof atlassian
Groups
Group ID Name 276f955c-63d7-42c8-9520-92d01dca0625 jdog-developers 6e87dc72-4f1f-421f-9382-2fee8b652487 juvenal-bot
jira-epic-issue-list
Get all issues that belong to an epic (Child Issues).
Scopes: read:epic:jira-software, read:issue-details:jira, read:jql:jira
Base Command
jira-epic-issue-list
Input
| Argument Name | Description | Required |
|---|---|---|
| epic_key | The key of the epic. (The epic ID or key is required). | Optional |
| epic_id | The ID of the epic. (The epic ID or key is required). | Optional |
| jql_query | A JQL query to filter the issues. | Optional |
| limit | The maximum number of results to return. This argument will be ignored if page or page_size arguments were given. Default value is 50. | Optional |
| page | The page number. Default value is 0. | Optional |
| page_size | The number of requested results per page. Default value is 50. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Jira.EpicIssues.boardId | String | The ID of the board that holds the issues. |
| Jira.EpicIssues.epicId | String | The ID of the epic that holds the issues. |
| Jira.EpicIssues.key | String | The key of the epic. |
| Jira.EpicIssues.Ticket.Id | Number | The ticket ID. |
| Jira.EpicIssues.Ticket.Key | String | The ticket key. |
| Jira.EpicIssues.Ticket.Assignee | String | The user assigned to the ticket. |
| Jira.EpicIssues.Ticket.Creator | String | The user who created the ticket. |
| Jira.EpicIssues.Ticket.Summary | String | The ticket summary. |
| Jira.EpicIssues.Ticket.Status | String | The ticket status. |
| Jira.EpicIssues.Ticket.Priority | String | The ticket priority. |
| Jira.EpicIssues.Ticket.ProjectName | String | The ticket project name. |
| Jira.EpicIssues.Ticket.DueDate | Date | The due date. |
| Jira.EpicIssues.Ticket.Created | Date | The time the ticket was created. |
| Jira.EpicIssues.Ticket.LastSeen | Date | The last time the ticket was viewed. |
| Jira.EpicIssues.Ticket.LastUpdate | Date | The last time the ticket was updated. |
Command example
!jira-epic-issue-list epic_key=PROJECTKEY-1 jql_query="status!=done"
Context Example
{
"Jira": {
"EpicIssues": {
"Ticket": [
{
"Assignee": "",
"Attachments": [
{
"created": "2023-01-22T16:43:29.878+0200",
"filename": "Screen Shot 2022-10-19 at 20.09.46.png",
"id": "16387",
"size": 450738
},
{
"created": "2023-02-05T21:11:43.429+0200",
"filename": "stam.png",
"id": "16399",
"size": 450738
}
],
"Components": [],
"Created": "2023-01-03T10:25:17.257+0200",
"Creator": "Example User(example@example.com)",
"Description": "This issue is for creating the home page.",
"DueDate": "",
"Id": "21361",
"Key": "PROJECTKEY-2",
"Labels": [
"Demisto",
"testnow"
],
"LastSeen": "",
"LastUpdate": "2023-02-26T18:24:04.172+0200",
"Priority": "Medium",
"ProjectName": "Company Snoozing App",
"Status": "Selected for Development",
"Summary": "Create Home Page (including the log in option)"
},
{
"Assignee": "",
"Attachments": [
{
"created": "2023-03-08T21:04:02.570+0200",
"filename": "DemistoContent-InstallContentEnvironment-160822-0023-68.pdf",
"id": "16401",
"size": 17815
},
{
"created": "2023-03-08T21:03:37.418+0200",
"filename": "Screen_Shot_2022-10-19_at_20.09.46 (2).png",
"id": "16400",
"size": 450738
}
],
"Components": [
"comp2",
"Integration"
],
"Created": "2023-03-07T17:31:48.225+0200",
"Creator": "Example User(example@example.com)",
"Description": "Dummy child description",
"DueDate": "",
"Id": "21496",
"Key": "PROJECTKEY-39",
"Labels": [
"Demisto",
"testnow"
],
"LastSeen": "",
"LastUpdate": "2023-05-04T21:46:22.076+0300",
"Priority": "Medium",
"ProjectName": "Company Snoozing App",
"Status": "Selected for Development",
"Summary": "Dummy child issue - delete later"
}
],
"epicId": "21360"
}
}
}
Human Readable Output
Child Issues in epic PROJECTKEY-1
Assignee Created Creator Description DueDate Id IssueType Key Labels Priority ProjectName Reporter Status Summary TicketLink 2023-01-03T10:25:17.257+0200 Example User(example@example.com) This issue is for creating the home page. 21361 Story PROJECTKEY-2 Demisto,
testnowMedium Company Snoozing App Example User(example@example.com) Selected for Development Create Home Page (including the log in option) https://api.atlassian.com/ex/jira/1234/rest/agile/1.0/issue/21361 2023-03-07T17:31:48.225+0200 Example User(example@example.com) Dummy child description 21496 Task PROJECTKEY-39 Demisto,
testnowMedium Company Snoozing App Example User(example@example.com) Selected for Development Dummy child issue - delete later https://api.atlassian.com/ex/jira/1234/rest/agile/1.0/issue/21496
jira-issue-link-type-get
Returns a list of all issue link types.
Scope: read:jira-work
Base Command
jira-issue-link-type-get
Input
There are no input arguments for this command.
Context Output
| Path | Type | Description |
|---|---|---|
| Jira.IssueLinkType | Object | The issue link type bean. |
| Jira.IssueLinkType.id | String | The ID of the issue link type. |
| Jira.IssueLinkType.inward | String | The description of the issue link type inward link. |
| Jira.IssueLinkType.name | String | The name of the issue link type. |
| Jira.IssueLinkType.outward | String | The description of the issue link type outward link. |
| Jira.IssueLinkType.self | String | The URL of the issue link type. |
Command example
#### Context Example
```json
{
"Jira": {
"IssueLinkType": [
{
"id": "10000",
"inward": "is blocked by",
"name": "Blocks",
"outward": "blocks",
"self": "https://api.atlassian.com/ex/jira/1234/rest/api/3/issueLinkType/10000"
},
{
"id": "10001",
"inward": "is cloned by",
"name": "Cloners",
"outward": "clones",
"self": "https://api.atlassian.com/ex/jira/1234/rest/api/3/issueLinkType/10001"
},
{
"id": "10002",
"inward": "is duplicated by",
"name": "Duplicate",
"outward": "duplicates",
"self": "https://api.atlassian.com/ex/jira/1234/rest/api/3/issueLinkType/10002"
},
{
"id": "10202",
"inward": "is reviewed by",
"name": "Post-Incident Reviews",
"outward": "reviews",
"self": "https://api.atlassian.com/ex/jira/1234/rest/api/3/issueLinkType/10202"
},
{
"id": "10201",
"inward": "is caused by",
"name": "Problem/Incident",
"outward": "causes",
"self": "https://api.atlassian.com/ex/jira/1234/rest/api/3/issueLinkType/10201"
},
{
"id": "10003",
"inward": "relates to",
"name": "Relates",
"outward": "relates to",
"self": "https://api.atlassian.com/ex/jira/1234/rest/api/3/issueLinkType/10003"
}
]
}
}
Human Readable Output
Issue Link Types
ID Inward Name Outward 10000 is blocked by Blocks blocks 10001 is cloned by Cloners clones 10002 is duplicated by Duplicate duplicates 10202 is reviewed by Post-Incident Reviews reviews 10201 is caused by Problem/Incident causes 10003 relates to Relates relates to
jira-issue-to-issue-link
Use this command to create a link between two issues.
Scope: write:jira-work
Base Command
jira-issue-to-issue-link
Input
| Argument Name | Description | Required |
|---|---|---|
| outward_issue | The key of the outward issue. | Required |
| inward_issue | The key of the inward issue. | Required |
| link_type | The name of the link to apply. Use jira-issue-link-type-get to see available links. |
Required |
| comment | A comment to add to the inward issue. | Optional |
Context Output
There is no context output for this command.
Command example
!jira-issue-to-issue-link inward_issue=PROJECTKEY-31 outward_issue=XSOAR-19 link_type="Post-Incident Reviews" comment="Dummy Link"
Human Readable Output
Issue link created successfully
fetch-incidents
Dummy fetch
Base Command
fetch-incidents
Input
There are no input arguments for this command.
Context Output
There is no context output for this command.
get-remote-data
Get remote data from a remote incident. This method does not update the current incident, and should be used for debugging purposes only.
Base Command
get-remote-data
Input
| Argument Name | Description | Required |
|---|---|---|
| id | The remote incident ID. | Required |
| lastUpdate | The UTC timestamp in seconds of the last update. 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 list of fields to map in outgoing mirroring. This command 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.
get-modified-remote-data
Get the list of incidents that were modified since the last update time. This method is used for debugging purposes. The get-modified-remote-data command is used as part of the Mirroring feature that was introduced in Cortex XSOAR version 6.1.
Base Command
get-modified-remote-data
Input
| Argument Name | Description | Required |
|---|---|---|
| lastUpdate | Date string representing the local time. The incident is only returned if it was modified after the last update time. | Optional |
Context Output
There is no context output for this command.
update-remote-system
Updates the remote incident with local 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.
jira-get-user-info
Retrieves information about a specified Jira user. For on-prem instances, you should use the user’s key and username to retrieve their details. For Cloud instances, user’s account_id is required. If no identifier is supplied, information for the user the API credentials belong to is returned.
Base Command
jira-get-user-info
Input
| Argument Name | Description | Required |
|---|---|---|
| key | The user’s key (On-prem only). | Optional |
| username | The user’s username (On-prem only). | Optional |
| account_id | The user’s account ID (Cloud only). | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Jira.Users.Key | Unknown | The user’s key or ID. |
| Jira.Users.Name | Unknown | Name of the user. |
| Jira.Users.Email | Unknown | User’s email address. |
| Jira.Users.Display Name | Unknown | Display name for the user. |
| Jira.Users.Active | Unknown | If the user is active or not. |
| Jira.Users.Deleted | Unknown | If the user is deleted or not. |
| Jira.Users.Timezone | Unknown | Timezone setting for the user. |
| Jira.Users.Locale | Unknown | Locale setting for the user. |
| Jira.Users.AccountID | Unknown | The account ID of the user, which uniquely identifies the user across all Atlassian products. For example, 5b10ac8d82e05b22cc7d4ef5. Required in requests. |
| Jira.Users.AccountType | Unknown | The user account type. Can take the following values: atlassian, app, customer. |
Command example
!jira-get-user-info key=JIRAUSER10000
Context Example
{
"Active": true,
"Deleted": false,
"Display Name": "John Doe",
"Email": "johndoe@example.com",
"Key": "JIRAUSER10000",
"Locale": "en_US",
"Name": "johndoe",
"Timezone": "America/Detroit"
}
jira-issue-get-forms
Fetches questions and answers for forms tied to a Jira issue. This command requires Jira Service Desk, the ProForma plugin and an API user with Service Desk Team or higher permissions on the Jira project the forms are being pulled from.
Base Command
jira-issue-get-forms
Input
| Argument Name | Description | Required |
|---|---|---|
| issue_id | The issue ID. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| JiraForms.ID | Unknown | The ID of the form. |
| JiraForms.Issue | Unknown | Issue the form belongs to. |
| JiraForms.Questions | Unknown | Questions for the form. |
Command example
!jira-issue-get-forms issue_id=TES-2
Context Example
{
"ID": 1,
"Issue": "TES-2",
"Name": "8/9/2024 New Form",
"Questions": [
{
"Answer": {
"choices": [
{
"id": "26",
"label": "I have read and agree to the terms and conditions."
}
]
},
"Description": "",
"ID": "8",
"Key": "q_terms",
"Label": "",
"Type": "cm"
},
{
"Answer": {},
"Description": "",
"ID": "4",
"Key": "q_email",
"Label": "Email",
"Type": "te"
},
{
"Answer": {},
"Description": "",
"ID": "5",
"Key": "q_request_for",
"Label": "Name of person needing building access",
"Type": "tl"
},
{
"Answer": {},
"Description": "",
"ID": "6",
"Key": "q_description",
"Label": "Request description",
"Type": "pg"
},
{
"Answer": {
"text": "John"
},
"Description": "",
"ID": "1",
"Key": "q_name",
"Label": "Name",
"Type": "ts"
},
{
"Answer": {
"text": "555-555-1212"
},
"Description": "",
"ID": "2",
"Key": "q_phone",
"Label": "Phone",
"Type": "ts"
},
{
"Answer": {},
"Description": "",
"ID": "7",
"Key": "q_reason",
"Label": "Reason for access",
"Type": "pg"
},
{
"Answer": {},
"Description": "",
"ID": "3",
"Key": "q_job_title",
"Label": "Job title",
"Type": "ts"
}
]
}
Incident Mirroring
You can enable incident mirroring between Cortex XSOAR incidents and Atlassian Jira V3 corresponding events (available from Cortex XSOAR version 6.0.0).
To set up the mirroring:
- Enable Fetching incidents in your instance configuration.
-
In the Mirroring Direction integration parameter, select in which direction the incidents should be mirrored:
Option Description None Turns off incident mirroring. Incoming Any changes in Atlassian Jira V3 events (mirroring incoming fields) will be reflected in Cortex XSOAR incidents. Outgoing Any changes in Cortex XSOAR incidents will be reflected in Atlassian Jira V3 events (outgoing mirrored fields). Incoming And Outgoing Changes in Cortex XSOAR incidents and Atlassian Jira V3 events will be reflected in both directions. - Optional: Check the Close Mirrored XSOAR Incident integration parameter to close the Cortex XSOAR incident when the corresponding event is closed in Atlassian Jira V3.
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.
Newly fetched incidents will be mirrored in the chosen direction. However, this selection does not affect existing incidents.
In order for the mirroring to work properly, please keep the timezone of the Jira profile public.
Important Note: To ensure the mirroring works as expected, mappers are required, both for incoming and outgoing, to map the expected fields in Cortex XSOAR and Atlassian Jira V3.
9 Incident Fields have been added/updated to enhance the mirroring experience
- dbotMirrorTags - Determines the tags that you need to add in Cortex XSOAR for entries to be pushed to Jira, and entries coming from Jira (Attachments or comments entries)
- You can set the tags for incoming entries in the instance configuration, using Attachment Entry Tag from Jira, and Comment Entry Tag from Jira.
- You can set the tags for outgoing entries in the instance configuration, using Attachment Entry Tag to Jira, and Comment Entry Tag to Jira.
- Jira Issue Type - Shows the type of the Jira issue.
- JiraV3 Status - Shows the status of the Jira issue. You can press on it in the JiraV3 Incident Layout to see the available statuses that you can transition into, and press on one of them to change the status of the issue.

- Jira Estimate - Shows the estimate of the Jira issue. The field can also be edited and mirrored out. Use the format 2w 4d 6h 45m (w - weeks, d - days, h - hours, m - minutes). Please add the
Time Trackingfield to the screen in your Jira projects in order for this field to work properly. - Jira Component - Shows the components of the Jira issue. The field can also be edited and mirrored out.
- Source Created By - Shows the display name, and email of the user who created the Jira issue.
- Jira Subtask - Shows the subtasks of the Jira issue in the form of a table.
- Jira Project - Shows the project name where the Jira issue resides in.
- Last Update Time - Shows the last updated time of the Jira issue.
- Jira Comment - Shows the comments of the Jira issue in the form of a table.
Modify the incoming mapper
- Navigate to Settings -> Objects Setup -> Classification and Mapping and click classifier-mapper-incoming-JiraV3.
- Under the Incident Type dropdown, select JiraV3 Incident.
- Change the mapping according to your needs.
- Save your changes.
Modify the outgoing mapper
- Under Classification and Mapping, click classifier-mapper-outgoing-Jira.
- 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 JiraV3 Incident and try again. - Under Schema Type, select JiraV3 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.

- Under the Incident Type dropdown, select JiraV3 Incident.
- On the right side of the screen, under Incident, select the incident based on which you want to
match. - Change the mapping according to your needs.
- Save your changes.
Configure the following integration parameters in order to customize the mirroring feature
- Mirror Direction:
- Mirror outgoing incidents: If enabled, any incident data changed in existing fetched incidents will be reflected in the remote Jira server.
- Mirror incoming incidents: If enabled, any incident data changed in the remote Jira server will be reflected in existing fetched incidents.
- Fetch incidents: Should be enabled in order to mirror in, and out new incidents.
- Incident type: In order to mirror out changes, and mirror in incoming changes, provide an incident type that is associated with a layout containing the fields you want to mirror. You can use ‘JiraV3 Incident’, which already has a built-in layout, and functionality fit for the
JiraV3 Incidenttype. - Attachment Entry Tag from Jira: Choose a tag to add to an attachment entry when mirroring in an attachment from Jira.
- Attachment Entry Tag to Jira: Choose a tag to add to an entry in order to mirror it as an attachment in Jira.
- Comment Entry Tag from Jira: Choose a tag to add to a note entry when mirroring in comments from Jira.
- Comment Entry Tag to Jira: Choose a tag to add to an entry in order to mirror it as a comment in Jira.
- Fetch Comments: Fetch comments for Jira ticket.
- Fetch Attachments: Fetch attachments for Jira ticket.
- Close Mirrored XSOAR Incident: Marking the Jira issue as resolved will be mirrored in Cortex XSOAR, and will close the corresponding incident. Please make sure the
Resolvedfield in your Jira instance is configured in order for this to work properly, if not, it will rely on if the status of the issue is changed toDone.
Simple Mirroring flow
- Create an incident in Jira. For the purpose of this use case, it can be a very simple incident. In Cortex XSOAR, the new ticket will be ingested in approximately one minute
- Add a note to the incident, which can be done using the
Add Commentsection in theJiraV3 Incident Layout. In the example below, we have written a comment from Cortex XSOAR to Jira.

- Add the Comment Entry Tag to Jira tag that you’ve configured in the instance before. The default is: “comment tag”.
- Add a file to the incident’s War Room.
- Click Actions -> Tags and add the Attachment Entry Tag to Jira tag that you’ve configured in the instance before. The default is: “attachment tag”.

- Go back to Incident Info and locate a field you’ve configured, or was already 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.

- Navigate back to the issue in Jira, and within approximately one minute, the changes will be reflected there as well.
- Change a Jira field you’ve configured in the incoming mapper.
- Go back to Cortex XSOAR and within approximately one minute, the changes will be reflected there as well.
Simple Comment and Attachment Mirroring flow
Mirror out Comment
- Press on the
Press to add comment to Jira issuebutton in theAdd Commentsection. - Add a comment and the tag that was configured in the Comment Entry Tag to Jira parameter.
- Once the comment is mirrored out to Jira, it will include the sentence Mirrored from Cortex XSOAR, to differentiate between comments mirrored from XSOAR, and normal comments.

Mirror in Comment
- Add a comment inside the Jira issue.
- The newly added comment will be mirrored in to XSOAR as a note, with the addition of the sentence Jira Author: {Author name}, to know the author of the comment, and tagged with the tag configured in Comment Entry Tag from Jira parameter.

Mirror out Attachment
- Add an attachment to the incident, and add the tag that was configured in the Attachment Entry Tag to Jira parameter.
- Once the attachment is reflected in Jira, the name of the file will have _mirrored_from_xsoar in it, to differentiate between attachments mirrored from XSOAR, and normal attachments.

Mirror in Attachment
- Once an attachment is mirrored in from Jira into XSOAR, the attachment will be tagged with the tag configured in Attahcment Entry Tag from Jira parameter.

Notes
- The final
source of truthof 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 JiraV3 Incident using the available transitions, please see the Change Ticket’s Status Using Transitions section below.
- If you wish to change the status of a JiraV3 Incident using the available statuses, please see the Change Ticket’s Status 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:
- Go to Settings -> Object Setup -> Incident Fields -> New Field
- Choose a field type. For example, for a label, you might want to use “Tag” as a type.
- Add a short description in the
Tooltipbox (not mandatory). - Click on
Attributes:
- UncheckAdd to incident typescheckbox.
- ForAdd to associated type, select the incident type you want to work with, preferably chooseJiraV3 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 “JiraV3 Incident Layout”:

Add the new field to the incoming mapper
- Configure the
Atlassian Jira V3integration, and test the connection of the instance as explained in the Authentication. - 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:
JiraV3 Incident. - The option
Fetch Incidentsis checked. - You have filled the respective parameters for the fetch to work, as explained in Fetch Incidents
- The
Mirror Directionis set to Incoming And Outgoing.
Finally, clickDone, and wait for new JiraV3 Incidents to be pulled:
- 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:
-
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: - Click on the button next to the
Mapper (incoming)field in order to edit the selected mapper. It will open theIncident 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 underName.
Once done, You'll see on the right side of the screen a JiraV3 Incident.- Note: If you want to work with a specific incident, in
Get dataselect the option:Upload JSONand 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 JiraV3 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_10097 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.
- Note: If you want to work with a specific incident, in
- Click “Save Version”.
Add the new field to the outgoing mapper
- Got to the configured
Atlassian Jira V3instance’s settings. - 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).
3. - Click on the button next to the
Mapper (outgoing)field in order to edit the selected mapper. It will open the “Incident Outgoing Mapping Editor”. - In “Select Instance” choose the name of your instance. You can find it in the integration settings under
Name. - Select one incident to work with by specifying it in the
Incidentlist.
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. - Search for the Jira field you want to map on the left side. In this example, we want to map customfield_10097

- Map the Jira field to the incident field you've created by clicking on
Select incident fieldand 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). - Click the “Save Version” button.
How to get an incident as JSON
- Use the !jira-get-issue command with the raw-response=true parameter. For example:
!jira-get-issue issue_id=VIK-183 raw-response=true - Click on
view full JSON in a new tab. - Click on
Download as a file. - Open the downloaded file and put its content inside a list, resulting in a list which contains only one item, the downloaded JSON.
- When selecting
Upload JSONforGet dataas mentioned above, you can upload the file using theUpload JSONoption.
Change Ticket’s Status Using Transitions or Statuses
- Make sure you’re using the ‘JiraV3 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 the script-JiraChangeTransition, script-JiraListTransition, JiraListStatus, and JiraChangeStatus scripts can work, which is crucial when you want to change the status of the issue. - In order to change an incident status, open the incident you would like to update.
- Under Incident Info search for the
Jira Transitions, orJira Statusfield. - Click on “Select” and choose the name of the transition or status to apply.


- Click on the ‘V’ option in the ‘V/X’ menu.
- You can see the new status under ‘Jira Status’.
- Go to Jira, and you will be able to see the new status there as well.
Breaking changes from the previous version of this integration - Atlassian Jira V3
The following sections list the changes in this version.
Commands
The following commands were removed in this version
-
jira-get-specific-field - This command was removed, and its functionality is implemented using the fields argument in the jira-get-issue command.
-
jira-append-to-field - This command was removed, and its functionality is implemented using the action argument when supplying it the value
appendin the jira-edit-issue command.
Arguments
The following arguments were removed in this version
In the jira-get-id-by-attribute command:
- is_jirav2api - The command does not require this argument anymore.
The behavior of the following arguments were changed
In the jira-create-issue command:
- issueJson - This argument will override the other arguments supplied to the command.
In the jira-edit-issue command:
- issueJson - This argument will override the other arguments supplied to the command.
In the jira-get-issue command:
- get_attachments - Supplying this argument with the value
truewill return the attachments found in the specified issue asEntry Info File, and not asFile.
jira-create-metadata-issue-types-list
Returns a page of issue type metadata for a specified project.
Base Command
jira-create-metadata-issue-types-list
Input
| Argument Name | Description | Required |
|---|---|---|
| project_id_or_key | The ID or key of the project. | Required |
| start_at | The index of the first item to return in a page of results (page offset). Default is 0. | Optional |
| max_results | The maximum number of items to return per page. Between 0 and 200. Default is 50. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Jira.IssueType.AvatarID | Number | The ID of the issue type’s avatar. |
| Jira.IssueType.Description | String | The description of the issue type. |
| Jira.IssueType.EntityID | String | Unique ID for next-gen projects. |
| Jira.IssueType.Expand | String | Expand options that include additional issue type metadata details in the response. |
| Jira.IssueType.IconURL | String | The URL of the issue type’s avatar. |
| Jira.IssueType.ID | String | The ID of the issue type. |
| Jira.IssueType.Name | String | The name of the issue type. |
| Jira.IssueType.Self | String | The URL of these issue type details. |
| Jira.IssueType.Subtask | Boolean | Whether this issue type is used to create subtasks. |
| Jira.IssueType.Scope | Object | Details of the next-gen projects the issue type is available in. |
| Jira.IssueType.Scope.type | String | The type of scope. Valid values: PROJECT, TEMPLATE. |
| Jira.IssueType.Scope.project | Object | The project the item has scope in. |
| Jira.IssueType.Scope.project.self | String | The URL of the project details. |
| Jira.IssueType.Scope.project.id | String | The ID of the project. |
| Jira.IssueType.Scope.project.key | String | The key of the project. |
| Jira.IssueType.Scope.project.name | String | The name of the project. |
| Jira.IssueType.Scope.project.projectTypeKey | String | The project type of the project. Valid values: software, service_desk, business. |
| Jira.IssueType.Scope.project.simplified | Boolean | Whether or not the project is simplified. |
| Jira.IssueType.Scope.project.avatarUrls | Object | The URLs of the project’s avatars. |
| Jira.IssueType.Scope.project.projectCategory | Object | The category the project belongs to. |
| Jira.IssueType.Scope.project.projectCategory.self | String | The URL of the project category. |
| Jira.IssueType.Scope.project.projectCategory.id | String | The ID of the project category. |
| Jira.IssueType.Scope.project.projectCategory.description | String | The name of the project category. |
| Jira.IssueType.Scope.project.projectCategory.name | String | The description of the project category. |
jira-create-metadata-field-list
Returns a page of field metadata for a specified project and issue type.
Base Command
jira-create-metadata-field-list
Input
| Argument Name | Description | Required |
|---|---|---|
| project_id_or_key | The ID or key of the project. | Required |
| issue_type_id | The issue type ID. | Required |
| start_at | The index of the first item to return in a page of results (page offset). Default is 0. | Optional |
| max_results | The maximum number of items to return per page. Between 0 and 200. Default is 50. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Jira.IssueField.AllowedValues | Array | The list of values allowed in the field. |
| Jira.IssueField.AutoCompleteUrl | String | The URL that can be used to automatically complete the field. |
| Jira.IssueField.Configuration | Object | The configuration properties. |
| Jira.IssueField.DefaultValue | String | The default value of the field. |
| Jira.IssueField.FieldID | String | The field ID. |
| Jira.IssueField.HasDefaultValue | Boolean | Whether the field has a default value. |
| Jira.IssueField.Key | String | The key of the field. |
| Jira.IssueField.Name | String | The name of the field. |
| Jira.IssueField.Operations | Array | The list of operations that can be performed on the field. |
| Jira.IssueField.Required | Boolean | Whether the field is required. |
| Jira.IssueField.Schema | Object | The data type for the field. |
| Jira.IssueField.Schema.type | String | The data type of the field. |
| Jira.IssueField.Schema.items | String | When the data type is an array, the name of the field items within the array. |
| Jira.IssueField.Schema.system | String | If the field is a system field, the name of the field. |
| Jira.IssueField.Schema.custom | String | If the field is a custom field, the URI of the field. |
| Jira.IssueField.Schema.customId | Number | If the field is a custom field, the custom ID of the field. |
| Jira.IssueField.Schema.configuration | Object | If the field is a custom field, the configuration of the field. |
jira-reset-timezone-cache
Clears the cached Jira user timezone used for fetching incidents and mirroring from the internal cache. Use if the timezone settings tied to the chosen authentication method were changed.
Base Command
jira-reset-timezone-cache
Input
There are no input arguments for this command.
Context Output
There is no context output for this command.
Command example
!jira-reset-timezone-cache
Human Readable Output
The Jira user timezone was successfully cleared from the cache
Configuration parameters
server_url— Server URL (required)basic_credentials— User namecloud_id— Cloud IDcallback_url— Callback URLcredentials— Client IDpat_credential— Personal Access Tokenfetch_query— Query (in JQL) for fetching incidentsissue_field_to_fetch_from— Issue Field to fetch byid_offset— Issue index to start fetching incidents fromfirst_fetch— Time range for initial data fetchinsecure— Trust any certificate (not secure)proxy— Use system proxy settingsisFetch— Fetch incidentsincidentType— Incident typeincidentFetchInterval— Incidents Fetch Intervalclose_incident— Close Mirrored XSOAR Incidentmirror_direction— Mirroring Directionattachment_tag_to_jira— Attachment Entry Tag to Jiraattachment_tag_from_jira— Attachment Entry Tag from Jiracomment_tag_to_jira— Comment Entry Tag to Jiracomment_tag_from_jira— Comment Entry Tag from Jirafetch_comments— Fetch commentsfetch_attachments— Fetch attachmentsmax_fetch— Max incidents per fetch
Commands (46)
-
get-mapping-fieldsReturns the list of fields to map in outgoing mirroring. This command is only used for debugging purposes.
-
get-modified-remote-dataGet the list of incidents that were modified since the last update time. This method is used for debugging purposes. The get-modified-remote-data command is used as part of the Mirroring feature that was introduced in Cortex XSOAR version 6.1.
-
get-remote-dataGet remote data from a remote incident. This method does not update the current incident, and should be used for debugging purposes only.
-
get-remote-data-previewGet remote data from a remote incident. This method does not update the current incident, and should be used for debugging purposes only.
-
jira-board-backlog-listGet issues from the backlog of a specific board. For Jira OnPrem, the board must be of type scrum.
-
jira-board-epic-listGet all epics from a specific board.
-
jira-board-issue-listGet all issues from a specific board.
-
jira-board-listGet board data. If board_id is given, then only the board that corresponds to the board_id is returned, otherwise, the the command will return boards that match the project_key_id, type, or board_name arguments.
-
jira-board-sprint-listGet all sprints of a specific board.
-
jira-create-issueCreates a new issue in Jira.
-
jira-create-issue-quick-actionCreates a new issue in Jira.
-
jira-create-metadata-field-listReturns a page of field metadata for a specified project and issue type.
-
jira-create-metadata-issue-types-listReturns a page of issue type metadata for a specified project.
-
jira-delete-issueDeletes an issue in Jira. Issue's sub-tasks will also be deleted if there are any.
-
jira-edit-issueModifies an issue in Jira. If the status or transition fields are given, then the other supplied fields should appear on the transition screen.
-
jira-epic-issue-listGet all issues that belong to an epic (Child Issues).
-
jira-get-commentsReturns the comments added to a ticket.
-
jira-get-id-by-attributeGets the account ID for a given user attribute.
-
jira-get-id-offsetReturns the ID offset, which is the ID of the first issue that was created in the current Jira instance, or the ID of the first issue with respect to the given query argument if given. You can use its result as the initial ID when fetching incidents by the issue id field.
-
jira-get-issueFetches an issue from Jira.
-
jira-get-specific-fieldGets specific fields from a Jira issue and adds it to context dynamically.
-
jira-get-user-infoRetrieves information about a specified Jira user. For on-prem instances, you should use the user's key and username to retrieve their details. For Cloud instances, user's `account_id` is required. If no identifier is supplied, information for the user the API credentials belong to is returned.
-
jira-issue-add-commentAdds a new comment to an existing Jira issue.
-
jira-issue-add-linkCreates (or updates) an issue link.
-
jira-issue-assignAssigns an assignee to an existing issue.
-
jira-issue-delete-commentDelete a comment from an issue.
-
jira-issue-delete-fileDelete an attachment from an issue.
-
jira-issue-edit-commentEdit a comment that is part of an issue.
-
jira-issue-get-attachmentDownload attachments for a specific issue (Will download a file to War Room).
-
jira-issue-get-formsFetches questions and answers for forms tied to a Jira issue. This command requires Jira Service Desk, the ProForma plugin and an API user with `Service Desk Team` or higher permissions on the Jira project the forms are being pulled from.
-
jira-issue-link-type-getReturns a list of all issue link types.
-
jira-issue-list-fieldsThis command returns the issue fields, both system and custom fields.
-
jira-issue-queryQueries Jira issues.
-
jira-issue-to-backlogMoves issues to backlog. If the issues supplied are NOT part of a board that supports sprints, then the board_id argument is required. At most 50 issues may be moved at once.
-
jira-issue-to-boardMoves issues from backlog to board, the command does not work with boards with sprints. At most 50 issues may be moved at once. This command is only supported for Jira Cloud instances.
-
jira-issue-to-issue-linkUse this command to create a link between two issues.
-
jira-issue-upload-fileUploads a file attachment to an issue.
-
jira-list-transitionsLists all possible transitions for a given ticket in its current status.
-
jira-oauth-completeUse this command to complete the authorization process. After copying the authorization code found in the query parameter `code` of the callback URL, paste the value in the command as an argument to finish the process.
-
jira-oauth-startUse this command to start the authorization process. In order to authorize the instance, first run the command, and complete the process in the URL that is returned. You will then be redirected to the callback URL where you will copy the authorization code found in the query parameter `code`, and paste that value in the command `!jira-ouath-complete` as an argument to finish the process.
-
jira-oauth-testUse this command to test the connectivity of the Jira instance.
-
jira-reset-timezone-cacheClears the cached Jira user timezone used for fetching incidents and mirroring from the internal cache. Use if the timezone settings tied to the chosen authentication method were changed.
-
jira-sprint-issue-listGet all issues in a sprint.
-
jira-sprint-issue-moveMoves issues to a sprint, for a given sprint ID. Issues can only be moved to open or active sprints. At most 50 issues may be moved at once.
-
jira-user-group-searchSearches for users and groups in Jira that match the query string.
-
update-remote-systemUpdates the remote incident with local incident changes. This method is only used for debugging purposes and will not update the current incident.
import json from unittest.mock import patch, Mock import demistomock as demisto import pytest from CommonServerPython import * from JiraV3 import JiraBaseClient, JiraCloudClient, JiraOnPremClient, get_remote_data_preview_command from pytest_mock import MockerFixture def util_load_json(path: str): with open(path, encoding="utf-8") as f: return json.loads(f.read()) def util_load_bytes_file(path: str): with open(path, mode="rb") as f: return f.read() # return json.loads(f.read()) @patch.object(JiraBaseClient, "__abstractmethods__", set()) def jira_base_client_mock(username: str = "", api_key: str = "", pat: str = "") -> JiraBaseClient: """The way to mock an abstract class is using the trick @patch.object(Abstract_Class, __abstractmethods__, set()), since Python, behind the scenes, checks the __abstractmethods__ property, which contains a set of the names of all the abstract methods defined on the abstract class, if it is not empty, we won't be able to instantiate the abstract class, however, if this set is empty, the Python interpreter will happily instantiate our class without any problems. """ return JiraBaseClient( base_url="https://dummy_url", proxy=False, verify=False, callback_url="dummy_callback", api_version="999", username=username, api_key=api_key, pat=pat, ) def jira_cloud_client_mock() -> JiraCloudClient: return JiraCloudClient( proxy=False, verify=False, client_id="dummy_client_id", client_secret="dummy_secret", callback_url="dummy_url", cloud_id="dummy_cloud_id", server_url="https://dummy_server_url", username="", api_key="", pat="", ) def jira_onprem_client_mock() -> JiraOnPremClient: return JiraOnPremClient( proxy=False, verify=False, client_id="dummy_client_id", client_secret="dummy_secret", callback_url="dummy_url", server_url="https://dummy_server_url", username="", api_key="", pat="", ) def test_v2_args_to_v3(): from JiraV3 import map_v2_args_to_v3 v2_args = { "startAt": "dummy_start_at", "maxResults": "dummy_max_results", "extraFields": "dummy_fields", "getAttachments": "dummy_get_attachments", "expandLinks": "dummy_expand_links", "expand_links": "not prioritized", "issueJson": "dummy_issue_json", "projectKey": "dummy_project_key", "issueTypeName": "dummy_issue_type_name", "issueTypeId": "dummy_issue_type_id", "projectName": "dummy_project_name", "dueDate": "dummy_due_date", "due_date": "not prioritized", "parentIssueKey": "dummy_parent_issue_key", "parentIssueId": "dummy_parent_issue_id", "attachmentName": "dummy_attachment_name", "globalId": "dummy_global_id", "applicationType": "dummy_application_type", "applicationName": "dummy_application_name", "issueId": "1234", "issueIdOrKey": "dummy_issue_key", } expected_v3_args = { "start_at": "dummy_start_at", "max_results": "dummy_max_results", "fields": "dummy_fields", "get_attachments": "dummy_get_attachments", "expand_links": "dummy_expand_links", "issue_json": "dummy_issue_json", "project_key": "dummy_project_key", "issue_type_name": "dummy_issue_type_name", "issue_type_id": "dummy_issue_type_id", "project_name": "dummy_project_name", "due_date": "dummy_due_date", "parent_issue_key": "dummy_parent_issue_key", "parent_issue_id": "dummy_parent_issue_id", "attachment_name": "dummy_attachment_name", "global_id": "dummy_global_id", "application_type": "dummy_application_type", "application_name": "dummy_application_name", "issue_id": "1234", "issue_key": "dummy_issue_key", } v3_args = map_v2_args_to_v3(args=v2_args) assert v3_args == expected_v3_args # Helper functions unit tests ADF_TEXT_CASES = [ ( "Hello there", {"type": "doc", "version": 1, "content": [{"type": "paragraph", "content": [{"text": "Hello there", "type": "text"}]}]}, ), ( '{"type": "doc", "version": 1, "content": [{"type": "paragraph", "content": [{"text": "Hello there", "type": "text"}]}]}', {"type": "doc", "version": 1, "content": [{"type": "paragraph", "content": [{"text": "Hello there", "type": "text"}]}]}, ), ( {"type": "doc", "version": 1, "content": [{"type": "paragraph", "content": [{"text": "Hello there", "type": "text"}]}]}, {"type": "doc", "version": 1, "content": [{"type": "paragraph", "content": [{"text": "Hello there", "type": "text"}]}]}, ), ( '{"invalid": "json"}', { "type": "doc", "version": 1, "content": [{"type": "paragraph", "content": [{"text": '{"invalid": "json"}', "type": "text"}]}], }, ), ( '{"type": "doc", "version": 1}', { "type": "doc", "version": 1, "content": [{"type": "paragraph", "content": [{"text": '{"type": "doc", "version": 1}', "type": "text"}]}], }, ), ( {"type": "doc", "version": 1, "content": ["invalid_node"]}, { "type": "doc", "version": 1, "content": [ { "type": "paragraph", "content": [{"text": {"type": "doc", "version": 1, "content": ["invalid_node"]}, "type": "text"}], } ], }, ), ] @pytest.mark.parametrize("text, expected_adf_text", ADF_TEXT_CASES) def test_text_to_adf(text, expected_adf_text): from JiraV3 import text_to_adf adf_text = text_to_adf(text=text) assert expected_adf_text == adf_text COMMENT_URL_CASES = [ ("https://dummy-domain.atlassian.net/rest/api/3/issue/10010/comment/10000", "10010"), ("http://dummy-domain.com/some_path/latest/issue/123/comment/", "123"), ] @pytest.mark.parametrize("url, expected_issue_id", COMMENT_URL_CASES) def test_extract_issue_id_from_comment_url(url, expected_issue_id): from JiraV3 import extract_issue_id_from_comment_url issue_id = extract_issue_id_from_comment_url(comment_url=url) assert expected_issue_id == issue_id ISSUE_FIELDS_MAPPING_CASES = [ ( [ { "id": "statuscategorychangedate", "key": "statuscategorychangedate", "name": "Status Category Changed", "custom": False, "orderable": False, "navigable": True, "searchable": True, "clauseNames": ["statusCategoryChangedDate"], "schema": {"type": "datetime", "system": "statuscategorychangedate"}, }, { "id": "parent", "key": "parent", "name": "Parent", "custom": False, "orderable": False, "navigable": True, "searchable": False, "clauseNames": ["parent"], }, ], {"statuscategorychangedate": "Status Category Changed", "parent": "Parent"}, ) ] @pytest.mark.parametrize("issue_fields, issue_fields_mapping", ISSUE_FIELDS_MAPPING_CASES) def test_get_issue_fields_mapping(mocker, issue_fields, issue_fields_mapping): from JiraV3 import JiraBaseClient, get_issue_fields_id_to_name_mapping mocker.patch.object(JiraBaseClient, "get_issue_fields", return_value=issue_fields) client = jira_base_client_mock() mapping_result = get_issue_fields_id_to_name_mapping(client=client) assert mapping_result == issue_fields_mapping CREATE_ISSUE_QUERY_CASES = [ ( "some_jql_string", None, None, {"jql": "some_jql_string", "maxResults": 50}, "", ), ( "some_jql_string", 12, None, {"jql": "some_jql_string", "startAt": 12, "maxResults": 50}, "", ), ( "some_jql_string", 1, 80, {"jql": "some_jql_string", "startAt": 1, "maxResults": 80}, "", ), ( "some_jql_string", 5, 80, {"jql": "some_jql_string", "nextPageToken": "dummy_page_token", "maxResults": 80}, "dummy_page_token", ), ] @pytest.mark.parametrize("jql, start_at, max_results, expected_query_params, next_page_token", CREATE_ISSUE_QUERY_CASES) def test_create_query_params(jql, start_at, max_results, expected_query_params, next_page_token): from JiraV3 import create_query_params query_params = create_query_params(jql_query=jql, start_at=start_at, max_results=max_results, next_page_token=next_page_token) assert query_params == expected_query_params PAGINATION_ARGS_CASES = [ ({"page": 1, "page_size": 3, "limit": 5}, {"start_at": 3, "max_results": 3}), ({"limit": 5}, {"start_at": 0, "max_results": 5}), ({"page": 4, "limit": 5}, {"start_at": 200, "max_results": 50}), ({"page_size": 23, "limit": 5}, {"start_at": 0, "max_results": 23}), ] @pytest.mark.parametrize("pagination_args, expected_parsed_pagination_args", PAGINATION_ARGS_CASES) def test_prepare_pagination_args(pagination_args, expected_parsed_pagination_args): from JiraV3 import prepare_pagination_args parsed_pagination_args = prepare_pagination_args(**pagination_args) assert expected_parsed_pagination_args == parsed_pagination_args @pytest.mark.parametrize("issue_id, issue_key", [("1234", "key1"), ("", "")]) def test_get_issue_id_or_key_error(issue_id, issue_key): from JiraV3 import get_issue_id_or_key with pytest.raises(DemistoException): get_issue_id_or_key(issue_id, issue_key) @pytest.mark.parametrize("issue_id, issue_key, expected_issue_id_or_key", [("1234", "", "1234"), ("", "key-1", "key-1")]) def test_get_issue_id_or_key(issue_id, issue_key, expected_issue_id_or_key): from JiraV3 import get_issue_id_or_key issue_id_or_key = get_issue_id_or_key(issue_id, issue_key) assert issue_id_or_key == expected_issue_id_or_key @pytest.mark.parametrize( "username, api_key, pat", [ ("dummy_username", "dummy_api_key", ""), ("", "", ""), ("", "", "dummy_pat"), ], ) def test_http_request(mocker, username: str, api_key: str, pat: str): """ Given: - username, api_key and pat When: - run http_request method Then: - Ensure when the username and api_key are provided then only the 'get_headers_with_basic_auth' method is called - Ensure when the username and api_key are not provided then only the 'get_headers_with_access_token' method is called """ client = jira_base_client_mock(username=username, api_key=api_key, pat=pat) basic_auth_mock = mocker.patch.object(client, "get_headers_with_basic_auth", return_value={}) oauth2_mock = mocker.patch.object(client, "get_headers_with_access_token", return_value={}) mocker.patch.object(client, "_http_request") client.http_request("GET") assert basic_auth_mock.call_count == int(bool(client.username)) assert oauth2_mock.call_count == int(not bool(client.username)) def test_test_module_basic_auth(mocker): """ Given: - mock client with username and api_key (basic auth) When: - run `jira_test_module` function Then: - Ensure no error is raised, and return `ok` """ from JiraV3 import jira_test_module client = jira_base_client_mock("dummy_username", "dummy_api_key") mocker.patch.object(client, "jira_test_instance_connection") assert jira_test_module(client, params={}) == "ok" def test_test_module_pat(mocker): """ Given: - mock client with personal access token (pat) When: - run `jira_test_module` function Then: - Ensure no error is raised, and return `ok` """ from JiraV3 import jira_test_module client = jira_base_client_mock(pat="dummy_pat") mocker.patch.object(client, "jira_test_instance_connection") assert jira_test_module(client, params={}) == "ok" def test_module_oauth2(mocker): """ Given: - mock client without username and api_key (oauth2) When: - run `jira_test_module` function Then: - Ensure that error msg is raised, with a guide how to connect through oauth2 """ from JiraV3 import jira_test_module client = jira_base_client_mock() mocker.patch.object(client, "jira_test_instance_connection") with pytest.raises(DemistoException, match="In order to authorize the instance, first run the command `!jira-oauth-start`."): jira_test_module(client, params={}) @pytest.mark.parametrize( "params, expected_exception", [ pytest.param( { "username": "", "api_key": "", "client_id": "", "client_secret": "", "pat": "", }, "The required parameters were not provided. See the help window for more information.", id="no auth params provided", ), pytest.param( { "username": "dummy_username", "api_key": "dummy_api_key", "client_id": "dummy_client_id", "client_secret": "dummy_client_secret", "pat": "dummy_pat", }, "The `User name` or `API key` parameters cannot be provided together with the `Client ID` or `Client Secret`" " parameters or with the `Personal Access Token` parameters. See the help window for more information.", id="multiple types of auth params are provided", ), pytest.param( { "username": "dummy_username", "api_key": "", "client_id": "", "client_secret": "", "pat": "", }, "To use basic authentication, the 'User name' and 'API key' parameters are mandatory", id="only `username` parameter was provided", ), pytest.param( { "username": "", "api_key": "", "client_id": "dummy_client_id", "client_secret": "", "pat": "", }, "To use OAuth 2.0, the 'Client ID' and 'Client Secret' parameters are mandatory", id="only `client_id` parameter was provided", ), ], ) def test_validate_params_failure(params: dict[str, str], expected_exception: str): """ Given: - auth params When: - run `validate_auth_params` function Then: - Ensure that as long as no valid auth params are sent an error is raised with a special message """ from JiraV3 import validate_auth_params with pytest.raises(DemistoException, match=expected_exception): validate_auth_params(**params) @pytest.mark.parametrize( "params", [ pytest.param( {"username": "dummy_username", "api_key": "dummy_api_key", "client_id": "", "client_secret": "", "pat": ""}, id="Only basic auth params were provided", ), pytest.param( {"username": "", "api_key": "", "client_id": "dummy_client_id", "client_secret": "dummy_client_secret", "pat": ""}, id="Only oauth2 params were provided oauth2", ), pytest.param( {"username": "", "api_key": "", "client_id": "", "client_secret": "", "pat": "dummy_pat_secret"}, id="Only pat param was provided pat", ), ], ) def test_validate_auth_params(params: dict[str, str]): """ Given: - auth params When: - run `validate_auth_params` function Then: - Ensure that when provided valid auth params the function does not raise """ from JiraV3 import validate_auth_params validate_auth_params(**params) @pytest.mark.parametrize("username, api_key", [("dummy_username", "dummy_api_key"), ("", "")]) def test_client_is_basic_auth_or_oauth(username: str, api_key: str): """ Given: - username and api_key When: - run `__init__` method for `JiraBaseClient` class Then: - Ensure that when the client class receives both username and api_key, the `is_basic_auth` flag is True otherwise False """ client = jira_base_client_mock(username, api_key) assert client.is_basic_auth == bool(username) class TestJiraGetIssueCommand: def test_create_file_info_from_attachment(self, mocker): """ Given: - An attachment id When - Calling the get create_file_info_from_attachment function to create a file of type EntryType.ENTRY_INFO_FILE Then - Validate that the file has been created, is of the correct type, has the correct file name, and was created with the correct content. """ from pathlib import Path from JiraV3 import create_file_info_from_attachment client = jira_base_client_mock() raw_response_attachment_metadata = util_load_json("test_data/get_issue_test/raw_response_attachment_metadata.json") dummy_attachment_content = util_load_bytes_file("test_data/get_issue_test/dummy_attachment_content.txt") mocker.patch.object(client, "get_attachment_metadata", return_value=raw_response_attachment_metadata) mocker.patch.object(client, "get_attachment_content", return_value=dummy_attachment_content) file_name = "dummy_file_name.pdf" file_result_mocker = mocker.patch("JiraV3.fileResult", side_effect=fileResult) file_info_res = create_file_info_from_attachment(client=client, attachment_id="dummy_attachment_id", file_name=file_name) assert file_result_mocker.call_args[1].get("data") == dummy_attachment_content assert file_info_res.get("Type") == EntryType.ENTRY_INFO_FILE assert file_info_res.get("File", "") == file_name assert Path.exists(Path(f"{demisto.investigation()['id']}_{file_info_res.get('FileID', '')}")) Path.unlink(Path(f"{demisto.investigation()['id']}_{file_info_res.get('FileID', '')}")) @pytest.mark.parametrize("get_attachments", [(True), (False)]) def test_download_issue_attachments_to_war_room(self, mocker, get_attachments): """ Given: - A boolean on whether to download the attachments from Jira to the war room or not. When - Calling the function that is in charge of downloading the attachments to the war room. Then - Validate that a fileResult object was created """ from JiraV3 import get_issue_command client = jira_base_client_mock() raw_issue_response = util_load_json("test_data/get_issue_test/raw_response.json") mocker.patch.object(client, "get_issue", return_value=raw_issue_response) mocker.patch( "JiraV3.create_file_info_from_attachment", return_value={ "Contents": "", "ContentsFormat": "dummy_format", "Type": "dummy_type", "File": "dummy_filename", "FileID": "dummy_id", }, ) demisto_results_mocker = mocker.patch.object(demisto, "results") get_issue_command(client=client, args={"issue_id": "1234", "get_attachments": get_attachments}) if get_attachments: demisto_results_mocker.assert_called_once() else: demisto_results_mocker.assert_not_called() def test_jira_get_issue(self, mocker): """ Given: - An issue key or id, and the arguments: expand_links=true, fields=watches,rank When - Calling the get issue command Then - Validate that the context data and human readable are correct. """ from JiraV3 import get_issue_command client = jira_base_client_mock() args = {"issue_key": "dummy_key", "get_attachments": "true", "expand_links": "true", "fields": "watches,rank"} raw_response = util_load_json("test_data/get_issue_test/raw_response.json") raw_response_extended_issues = util_load_json("test_data/get_issue_test/raw_response_extended_issues.json") expected_command_results_context = util_load_json("test_data/get_issue_test/parsed_result.json") mocker.patch.object(client, "get_issue", return_value=raw_response) mocker.patch("JiraV3.get_expanded_issues", return_value=raw_response_extended_issues) mocker.patch("JiraV3.download_issue_attachments_to_war_room", return_value=None) command_results = get_issue_command(client, args) for expected_command_result_context, command_result in zip(expected_command_results_context, command_results): assert expected_command_result_context["EntryContext"] == command_result.to_context()["EntryContext"] assert expected_command_result_context["HumanReadable"] == command_result.to_context()["HumanReadable"] class TestJiraGetCommentsCommand: def test_jira_get_comments(self, mocker): """ Given: - An issue key or id. When - Calling the get comments command. Then - Validate that the context data and human readable are correct. """ from JiraV3 import get_comments_command client = jira_base_client_mock() raw_response = util_load_json("test_data/get_comments_test/raw_response.json") expected_command_results_context = util_load_json("test_data/get_comments_test/parsed_result.json") mocker.patch.object(client, "get_comments", return_value=raw_response) command_result = get_comments_command(client=client, args={"issue_key": "dummy_issue_key"}) assert expected_command_results_context["EntryContext"] == command_result.to_context()["EntryContext"] assert expected_command_results_context["HumanReadable"] == command_result.to_context()["HumanReadable"] def test_extract_comment_entry_from_raw_response(self): """ Given: - A comment that has been returned from the Jira API. When - Extracting the comment entry from the raw response. Then - Validate that the comment entry includes the correct values. """ from JiraV3 import extract_comment_entry_from_raw_response comment_raw_response = { "id": "18322", "author": { "displayName": "Example User", }, "body": { "version": 1, "type": "doc", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Hello there"}]}], }, "renderedBody": "<p>Hello there</p>", "updateAuthor": { "displayName": "Example User", }, "created": "2023-03-23T07:45:29.056+0200", "updated": "2023-03-23T07:45:29.056+0200", } expected_comment_entry = { "Id": "18322", "Comment": "Hello there", "User": "Example User", "Created": "2023-03-23T07:45:29.056+0200", "Updated": "2023-03-23T07:45:29.056+0200", "UpdateUser": "Example User", } comment_entry = extract_comment_entry_from_raw_response(comment_response=comment_raw_response) assert comment_entry == expected_comment_entry class TestJiraEditIssueCommand: def test_edit_issue_with_transition_and_status_error(self): """ Given: - A Jira client, and arguments that hold the status and transition supplied by the user. When - Calling the edit issue command. Then - Validate that an error is returned since the user cannot supply both a status and transition. """ from JiraV3 import edit_issue_command client = jira_base_client_mock() with pytest.raises(DemistoException) as e: edit_issue_command(client=client, args={"issue_id": "1", "status": "dummy_status", "transition": "dummy_transition"}) assert "Please provide only status or transition, but not both" in str(e) def test_edit_issue_command(self, mocker): """ Given: - A Jira client, and arguments to edit a Jira issue (without status and transition arguments). When - Calling the edit issue command. Then - Validate that the edit_issue method was called, then get_issue was called to retrieve the newly updated issue, and that the updated issue's data was returned to context data. """ from JiraV3 import create_issue_md_and_outputs_dict, edit_issue_command client = jira_base_client_mock() args = {"issue_key": "dummy_key", "due_date": "2024-05-07"} mocker.patch.object(client, "edit_issue", return_value=requests.Response()) dummy_issue_data = {"id": "1234", "key": "dummy_key", "fields": {"duedate": "2024-05-07"}} mocker.patch.object(client, "get_issue", return_value=dummy_issue_data) _, outputs = create_issue_md_and_outputs_dict(issue_data=dummy_issue_data) command_result = edit_issue_command(client=client, args=args) assert command_result.to_context().get("EntryContext") == {"Ticket(val.Id && val.Id == obj.Id)": outputs} @pytest.mark.parametrize( "args", [ ({"issue_key": "dummy_key", "status": "Selected for development"}), ({"issue_key": "dummy_key", "transition": "In Development"}), ], ) def test_apply_issue_status_and_transition(self, mocker, args): """ Given: - A Jira client, and the status, or transition argument to change the status of the issue. When - Calling the edit issue command. Then - Validate that get_transitions, and transition_issue method were called, which is in charge of changing the status of the issue. """ from JiraV3 import edit_issue_command client = jira_base_client_mock() transitions_raw_response = util_load_json("test_data/get_transitions_test/raw_response.json") get_transitions_mocker = mocker.patch.object(client, "get_transitions", return_value=transitions_raw_response) apply_transition_mocker = mocker.patch.object(client, "transition_issue", return_value=requests.Response()) mocker.patch.object(client, "get_issue", return_value={}) mocker.patch.object(client, "edit_issue", return_value=requests.Response()) edit_issue_command(client=client, args=args) get_transitions_mocker.assert_called_once() apply_transition_mocker.assert_called_once() @pytest.mark.parametrize( "args", [ ({"issue_key": "dummy_key", "status": "Selected for development"}), ({"issue_key": "dummy_key", "transition": "In Development"}), ], ) def test_apply_issue_status_and_transition_with_arguments(self, mocker, args): """ Given: - A Jira client, and the status, or transition argument to change the status of the issue. When - Calling the edit issue command, with additional arguments to edit the issue. Then - Validate that correct issue fields were sent as part of the request. """ from JiraV3 import edit_issue_command client = jira_base_client_mock() transitions_raw_response = util_load_json("test_data/get_transitions_test/raw_response.json") mocker.patch.object(client, "get_transitions", return_value=transitions_raw_response) mocker.patch.object(client, "transition_issue", return_value=requests.Response()) command_args = args | { "issue_key": "dummy_key", "description": "dummy description", "project_key": "dummy_project_key", "project_id": "dummy_project_id", "labels": "label1,label2", "components": "comp1,comp2", "customfield_1": "dummy custom field", } # The transition ID is 21 since the mocked transition 'In Development' has an ID of 21 and the status # 'Selected for development' correlates to the transition 'In Development', which as stated, has an ID of 21 expected_issue_fields = { "transition": {"id": "21"}, "fields": { "description": "dummy description", "project": {"key": "dummy_project_key", "id": "dummy_project_id"}, "labels": ["label1", "label2"], "components": [{"name": "comp1"}, {"name": "comp2"}], "customfield_1": "dummy custom field", }, } mocker.patch.object(client, "get_issue", return_value={}) transition_issue_mocker = mocker.patch.object(client, "transition_issue", return_value=requests.Response()) edit_issue_command(client=client, args=command_args) assert expected_issue_fields == transition_issue_mocker.call_args[1].get("json_data") def test_create_issue_fields_with_action_rewrite(self, mocker): """ Given: - A Jira client, and issue fields to edit the Jira issue, with the rewrite action. When - Calling the edit issue command. Then - Validate that the edit_issue method (which is in charge of calling the endpoint with the relevant data to edit the issue) was called with the correct json data. """ from JiraV3 import edit_issue_command client = jira_base_client_mock() args = { "issue_key": "dummy_key", "description": "dummy description", "project_key": "dummy_project_key", "project_id": "dummy_project_id", "labels": "label1,label2", "components": "comp1,comp2", "customfield_1": "dummy custom field", } expected_issue_fields = { "fields": { "description": "dummy description", "project": {"key": "dummy_project_key", "id": "dummy_project_id"}, "labels": ["label1", "label2"], "components": [{"name": "comp1"}, {"name": "comp2"}], "customfield_1": "dummy custom field", } } mocker.patch.object(client, "get_issue", return_value={}) edit_issue_mocker = mocker.patch.object(client, "edit_issue", return_value=requests.Response()) edit_issue_command(client=client, args=args) assert expected_issue_fields == edit_issue_mocker.call_args[1].get("json_data") def test_create_issue_fields_for_update_with_action_append(self, mocker): """ Given: - A Jira client, and issue fields to edit the Jira issue, with the append action. When - Calling the edit issue command. Then - Validate that the edit_issue method (which is in charge of calling the endpoint with the relevant data to edit the issue) was called with the correct json data. """ from JiraV3 import edit_issue_command client = jira_base_client_mock() args = { "issue_key": "dummy_key", "components": "comp1,comp2", "labels": "label1,label2", "summary": "appended summary", "action": "append", } expected_issue_fields = { "fields": { "components": [{"name": "current-comp1"}, {"name": "current-comp2"}, {"name": "comp1"}, {"name": "comp2"}], "labels": ["current-label1", "current-label2", "label1", "label2"], "summary": "current summary, appended summary", } } mocker.patch.object( client, "get_issue", side_effect=[ { "fields": { "components": [{"name": "current-comp1"}, {"name": "current-comp2"}], "labels": ["current-label1", "current-label2"], "summary": "current summary", } }, {}, ], ) edit_issue_mocker = mocker.patch.object(client, "edit_issue", return_value=requests.Response()) edit_issue_command(client=client, args=args) assert expected_issue_fields == edit_issue_mocker.call_args[1].get("json_data") def test_create_custom_issue_fields_for_update_with_action_append(self, mocker): """ Given: - A Jira client, and custom issue fields (supplied using the issue_json argument) to edit the Jira issue, with the append action. When - Calling the edit issue command. Then - Validate that the edit_issue method (which is in charge of calling the endpoint with the relevant data to edit the issue) was called with the correct json data. """ from JiraV3 import edit_issue_command client = jira_base_client_mock() args = { "issue_key": "dummy_key", "issue_json": '{"fields": {"customfield_1": "new data", "customfield_2": ["new data"]}}', "action": "append", } expected_issue_fields = {"fields": {"customfield_1": "old data, new data", "customfield_2": ["old data", "new data"]}} mocker.patch.object( client, "get_issue", side_effect=[{"fields": {"customfield_1": "old data", "customfield_2": ["old data"]}}, {}] ) edit_issue_mocker = mocker.patch.object(client, "edit_issue", return_value=requests.Response()) edit_issue_command(client=client, args=args) assert expected_issue_fields == edit_issue_mocker.call_args[1].get("json_data") def test_edit_issue_command_with_issue_json_and_another_arg_error(self): from JiraV3 import edit_issue_command client = jira_base_client_mock() with pytest.raises( DemistoException, match=( "When using the `issue_json` argument, additional arguments cannot be used " "except `issue_id`, `issue_key`, `status`, `transition`, and `action` arguments.ֿֿֿ" "\n see the argument description" ), ): edit_issue_command( client=client, args={"summary": "test", "issue_json": '{"fields": {"customfield_10037":"field_value"}}'} ) @pytest.mark.parametrize( "extra_args", [ {"action": "test"}, {"status": "test"}, {"transition": "test"}, {"issue_key": "test"}, {"issue_id": "test"}, ], ) def test_edit_issue_command_with_issue_json_and_another_arg_no_error(self, mocker: MockerFixture, extra_args: dict): """ Given: - The `issue_json` arg and one more arg allowed for use with `issue_json` When: - run edit_issue_command function Then: - Ensure that the validation process, which ensures that no additional arguments are present alongside the 'issue_json' argument, does not result in an error in cases where the additional arguments are one of: `action`, `status`, `transition`. """ from JiraV3 import edit_issue_command client = jira_base_client_mock() mocker.patch("JiraV3.apply_issue_status") mocker.patch("JiraV3.apply_issue_transition") mocker.patch.object(client, "edit_issue") mocker.patch.object(client, "get_issue", return_value={}) mocker.patch("JiraV3.create_issue_md_and_outputs_dict", return_value=({}, {})) mocker.patch("JiraV3.create_issue_fields", return_value={}) mocker.patch("JiraV3.create_issue_fields_for_appending", return_value={}) mocker.patch("JiraV3.get_issue_id_or_key", return_value="test") args = {"issue_json": '{"fields": {"customfield_10037":"field_value"}}'} | extra_args assert edit_issue_command(client=client, args=args) class TestJiraCreateIssueCommand: def test_create_issue_command(self, mocker): """ Given: - A Jira client When - Calling the create issue command. Then - Validate that the issue id and key of the newly created issue is returned. """ from JiraV3 import create_issue_command client = jira_base_client_mock() raw_response = {"id": "1234", "key": "dummy_key", "self": "dummy_link"} expected_outputs = {"Id": "1234", "Key": "dummy_key"} expected_mo_outputs = { "object_id": "1234", "object_name": "dummy_key", "object_url": "https://example.com/browse/dummy_key", } mocker.patch.object(client, "create_issue", return_value=raw_response) mocker.patch.object(demisto, "results") command_results = create_issue_command( client=client, args={"summary": "test"}, is_quick_action=True, server_url="https://example.com" ) assert command_results[0].to_context().get("EntryContext") == {"Ticket(val.Id && val.Id == obj.Id)": expected_outputs} assert demisto.results.call_args[0][0]["ExtendedPayload"].get("MirrorObject") == expected_mo_outputs def test_create_issue_command_with_issue_json(self, mocker): """ Given: - A Jira client - Jira summary from issue_json When - Calling the create issue command. Then - Validate that the issue id and key of the newly created issue is returned. """ from JiraV3 import create_issue_command client = jira_base_client_mock() raw_response = {"id": "1234", "key": "dummy_key", "self": "dummy_link"} expected_outputs = {"Id": "1234", "Key": "dummy_key"} expected_mo_outputs = { "object_id": "1234", "object_name": "dummy_key", "object_url": "http://example.com/browse/dummy_key", } mocker.patch.object(client, "create_issue", return_value=raw_response) mocker.patch.object(demisto, "results") command_results = create_issue_command( client=client, args={"issue_json": '{"fields": {"summary": "test"}}'}, is_quick_action=True, server_url="http://example.com", ) assert command_results[0].to_context().get("EntryContext") == {"Ticket(val.Id && val.Id == obj.Id)": expected_outputs} assert demisto.results.call_args[0][0]["ExtendedPayload"].get("MirrorObject") == expected_mo_outputs def test_create_issue_command_with_issue_json_and_another_arg(self): """ Given: - A Jira client - issue_json and summary args When - Calling the create issue command. Then - Ensure an error is raised with an expected error message. """ from JiraV3 import create_issue_command client = jira_base_client_mock() with pytest.raises( DemistoException, match="When using the argument `issue_json`, additional arguments should be placed within the `fields` in the json.", ): create_issue_command( client=client, args={"summary": "test", "issue_json": '{"fields": {"customfield_10037":"field_value"}}'} ) def test_create_issue_command_no_summary(self): """ Given: - A Jira client - no Jira summary from issue_json / args When - Calling the create issue command. Then - Validate that DemistoException is raised """ from JiraV3 import create_issue_command client = jira_base_client_mock() with pytest.raises(DemistoException) as e: create_issue_command(client=client, args={}) assert "The summary argument must be provided" in str(e) class TestJiraDeleteIssueCommand: def test_delete_issue_command(self, mocker): """ Given: - A Jira client When - Calling the delete issue command. Then - Validate that the correct readable output is outputted to the user. """ from JiraV3 import delete_issue_command client = jira_base_client_mock() mocker.patch.object(client, "delete_issue", return_value=requests.Response()) command_result = delete_issue_command(client=client, args={"issue_key": "dummy_key"}) assert "Issue deleted successfully" in command_result.to_context().get("HumanReadable") class TestJiraDeleteAttachmentFileCommand: def test_delete_attachment_file_command(self, mocker: MockerFixture): """ Given: - A Jira client. When - Calling the delete attachment file command. Then - Validate that the correct readable output is outputted to the user. """ from JiraV3 import delete_attachment_file_command attachment_id = "dummy_id" client = jira_base_client_mock() mocker.patch.object(client, "delete_attachment_file", return_value=requests.Response()) command_result = delete_attachment_file_command(client=client, args={"attachment_id": attachment_id}) assert f"Attachment id {attachment_id} was deleted successfully" in command_result.to_context().get("HumanReadable") class TestJiraGetTransitionsCommand: def test_get_transitions_command(self, mocker): """ Given: - A Jira client When - Calling the get_transitions_command Then - Validate that the correct CommandResult object is returned to the user. """ from JiraV3 import get_transitions_command client = jira_base_client_mock() raw_response = util_load_json("test_data/get_transitions_test/raw_response.json") expected_command_results_context = util_load_json("test_data/get_transitions_test/parsed_result.json") mocker.patch.object(client, "get_transitions", return_value=raw_response) command_result = get_transitions_command(client=client, args={"issue_key": "dummy_key"}) assert expected_command_results_context["EntryContext"] == command_result.to_context()["EntryContext"] assert expected_command_results_context["HumanReadable"] == command_result.to_context()["HumanReadable"] class TestJiraAddCommentCommand: def test_add_comment_command(self, mocker): """ Given: - A Jira client When - Calling the add_comment_command Then - Validate that the correct CommandResult object is returned to the user. """ from JiraV3 import add_comment_command client = jira_base_client_mock() args = {"visibility": "Administrators", "issue_key": "dummy_key", "comment": "dummy_comment"} raw_response = util_load_json("test_data/create_comment_test/raw_response.json") expected_command_results_context = util_load_json("test_data/create_comment_test/parsed_result.json") mocker.patch.object(client, "add_comment", return_value=raw_response) command_result = add_comment_command(client=client, args=args) assert expected_command_results_context["EntryContext"] == command_result.to_context()["EntryContext"] assert expected_command_results_context["HumanReadable"] == command_result.to_context()["HumanReadable"] class TestJiraGetIDOffsetCommand: def test_get_id_offset_command(self, mocker): """ Given: - A Jira client When - Calling the get_id_offset_command, in order to retrieve the first issue id in the Jira instance. Then - Validate that the correct CommandResult object is returned to the user, and that the correct JQL was sent to in order to retrieve the ID. """ from JiraV3 import get_id_offset_command client = jira_base_client_mock() raw_response = util_load_json("test_data/issue_query_test/raw_response.json") run_query_mocker = mocker.patch.object(client, "run_query", return_value=raw_response) command_result = get_id_offset_command(client=client, args={}) assert run_query_mocker.call_args[1].get("query_params", {}).get("jql") == "ORDER BY created ASC" assert command_result.to_context()["EntryContext"] == {"Ticket": {"idOffSet": "10161"}} def test_get_id_offset_command_with_custom_query_argument(self, mocker): """ Given: - A Jira client When - Calling the get_id_offset_command, with the argument `query` in order to retrieve the first issue id with respect to the given query. Then - Validate that the correct query is being sent with the API call. """ from JiraV3 import get_id_offset_command client = jira_base_client_mock() raw_response = util_load_json("test_data/issue_query_test/raw_response.json") run_query_mocker = mocker.patch.object(client, "run_query", return_value=raw_response) get_id_offset_command(client=client, args={"query": 'project = "Dummy Project"'}) assert ( run_query_mocker.call_args[1].get("query_params", {}).get("jql") == 'project = "Dummy Project" ORDER BY created ASC' ) def test_get_id_offset_empty_results(self, mocker): """ Given: - A Jira client When - Calling the get_id_offset_command, and getting no issues from the API. Then - Validate that the correct message is returned to the user. """ from JiraV3 import get_id_offset_command client = jira_base_client_mock() mocker.patch.object(client, "run_query", return_value={}) command_result = get_id_offset_command(client=client, args={}) assert command_result.to_context().get("HumanReadable") == "No issues found to retrieve the ID offset" def test_edit_comment_command(self, mocker): """ Given: - A Jira client When - Calling the edit_comment_command. Then - Validate that the get_comments method is called to retrieve the comments of the issue, that includes the newly edited comment, and that the correct outputs is returned to the context data. """ from JiraV3 import edit_comment_command client = jira_base_client_mock() comments_raw_response = util_load_json("test_data/get_comments_test/raw_response.json") expected_command_results_context = util_load_json("test_data/get_comments_test/parsed_result.json") mocker.patch.object(client, "edit_comment", return_value=requests.Response()) get_comments_mocker = mocker.patch.object(client, "get_comments", return_value=comments_raw_response) command_result = edit_comment_command(client=client, args={"issue_key": "dummy_issue_key"}) get_comments_mocker.assert_called_once() assert expected_command_results_context["EntryContext"] == command_result.to_context()["EntryContext"] class TestJiraListIssueFieldsCommand: @pytest.mark.parametrize("pagination_args", [({"start_at": 0, "max_results": 2}), ({"start_at": 1, "max_results": 3})]) def test_list_fields_command(self, mocker, pagination_args): """ Given: - A Jira client When - Calling the jira-issue-list-fields command. Then - Validate that correct pagination has been applied, since the API endpoint does not support pagination, and we do it manually. """ from JiraV3 import list_fields_command client = jira_base_client_mock() issue_fields_raw_response = util_load_json("test_data/get_issue_fields_test/raw_response.json") expected_context = util_load_json("test_data/get_issue_fields_test/parsed_result.json") start_at = pagination_args.get("start_at", 0) max_results = pagination_args.get("max_results", 50) mocker.patch.object(client, "get_issue_fields", return_value=issue_fields_raw_response) mocker.patch("JiraV3.prepare_pagination_args", return_value=pagination_args) command_result = list_fields_command(client=client, args={"issue_key": "dummy_issue_key"}) # [start_at: start_at + max_results] is the way do the pagination manually, therefore we check it. expected_outputs = expected_context["EntryContext"]["Jira.IssueField(val.id && val.id == obj.id)"][ start_at : start_at + max_results ] assert expected_outputs == command_result.to_context()["EntryContext"]["Jira.IssueField(val.id && val.id == obj.id)"] class TestJiraIssueToBacklogCommand: @pytest.mark.parametrize( "args", [({"rank_before_issue": "key1", "issues": "issue1,issue2"}), ({"rank_after_issue": "key1", "issues": "issue1,issue2"})], ) def test_using_rank_without_board_id_error(self, args): """ Given: - A Jira client When - Calling the jira-issue-to-backlog command, with the rank_before_issue, or rank_after_issue arguments, without the board_id argument. Then - Validate that an error is thrown. """ from JiraV3 import issues_to_backlog_command client = jira_base_client_mock() with pytest.raises(DemistoException) as e: issues_to_backlog_command(client=client, args=args) assert "Please supply the board_id argument" in str(e) def test_issues_to_backlog_is_called_when_using_board_id(self, mocker): """ Given: - A Jira client When - Calling the jira-issue-to-backlog command, with the board_id argument. Then - Validate that the correct API call is being called (issues_to_backlog, which is available only for Jira Cloud). """ from JiraV3 import issues_to_backlog_command client = jira_cloud_client_mock() issues_to_backlog_mocker = mocker.patch.object(client, "issues_to_backlog", return_value=requests.Response()) issues_to_backlog_command(client=client, args={"board_id": "dummy_board_id"}) issues_to_backlog_mocker.assert_called_once() def test_using_board_id_with_onprem_error(self): """ Given: - A Jira client When - Calling the jira-issue-to-backlog command, with the board_id argument, on an OnPrem instance. Then - Validate that an error is thrown, since only Jira Cloud supports the board_id argument. """ from JiraV3 import issues_to_backlog_command client = jira_onprem_client_mock() with pytest.raises(DemistoException) as e: issues_to_backlog_command(client=client, args={"board_id": "dummy_board_id"}) assert "The argument board_id is not supported for a Jira OnPrem instance" in str(e) def test_issues_to_backlog_command(self, mocker): """ Given: - A Jira client When - Calling the jira-issue-to-backlog command. Then - Validate that the correct API call is being called. """ from JiraV3 import issues_to_backlog_command client = jira_base_client_mock() issues_from_sprint_to_backlog_mocker = mocker.patch.object( client, "issues_from_sprint_to_backlog", return_value=requests.Response() ) command_results = issues_to_backlog_command(client=client, args={}) issues_from_sprint_to_backlog_mocker.assert_called_once() assert command_results.to_context()["HumanReadable"] == "Issues were moved to Backlog successfully" class TestJiraIssuesToBoardCommand: def test_issues_to_board_with_onprem_error(self): """ Given: - A Jira client When - Calling the jira-issue-to-board command, with an OnPrem instance. Then - Validate that an error is thrown, since this command is only supported by Jira Cloud. """ from JiraV3 import issues_to_board_command client = jira_onprem_client_mock() with pytest.raises(DemistoException) as e: issues_to_board_command(client=client, args={}) assert "This command is not supported by a Jira OnPrem instance" in str(e) def test_issues_to_board_command(self, mocker): """ Given: - A Jira client When - Calling the jira-issue-to-board command, with a Cloud instance. Then - Validate that the correct CommandResults is returned to the user. """ from JiraV3 import issues_to_board_command client = jira_cloud_client_mock() mocker.patch.object(client, "issues_to_board", return_value=requests.Response()) command_results = issues_to_board_command(client=client, args={}) assert command_results.to_context()["HumanReadable"] == "Issues were moved to Board successfully" class TestJiraBoardListCommand: def test_get_board_using_board_id_command(self, mocker): """ Given: - A Jira client When - Calling the jira-board-list command, with the board_id argument. Then - Validate that the correct API call is called to retrieve the specific data corresponding to the board id. """ from JiraV3 import board_list_command client = jira_base_client_mock() board_raw_response = util_load_json("test_data/get_board_test/raw_response.json") expected_command_results = util_load_json("test_data/get_board_test/parsed_result.json") get_board_mocker = mocker.patch.object(client, "get_board", return_value=board_raw_response) get_boards_mocker = mocker.patch.object(client, "get_boards", return_value={}) command_results = board_list_command(client=client, args={"board_id": "dummy_board_id"}) get_board_mocker.assert_called_once() get_boards_mocker.assert_not_called() assert expected_command_results["EntryContext"] == command_results.to_context()["EntryContext"] def test_get_boards_command(self, mocker): """ Given: - A Jira client When - Calling the jira-board-list command, without the board_id argument. Then - Validate that the correct API call is called to retrieve the specific data. """ from JiraV3 import board_list_command client = jira_base_client_mock() board_raw_response = util_load_json("test_data/get_boards_test/raw_response.json") expected_command_results = util_load_json("test_data/get_boards_test/parsed_result.json") get_board_mocker = mocker.patch.object(client, "get_board", return_value={}) get_boards_mocker = mocker.patch.object(client, "get_boards", return_value=board_raw_response) command_results = board_list_command(client=client, args={}) get_board_mocker.assert_not_called() get_boards_mocker.assert_called_once() assert expected_command_results["EntryContext"] == command_results.to_context()["EntryContext"] class TestJiraIssuesFromBacklogOfBoardCommand: def test_get_issues_from_backlog_of_board_command(self, mocker): """ Given: - A Jira client When - Calling the jira-board-backlog-list command. Then - Validate that the correct context data is returned to the user. """ from JiraV3 import board_backlog_list_command client = jira_base_client_mock() backlog_issues_raw_response = util_load_json("test_data/get_issues_from_backlog_test/raw_response.json") expected_command_results = util_load_json("test_data/get_issues_from_backlog_test/parsed_result.json") mocker.patch.object(client, "get_issues_from_backlog", return_value=backlog_issues_raw_response) command_results = board_backlog_list_command(client=client, args={"board_id": "14"}) assert expected_command_results["EntryContext"] == command_results.to_context()["EntryContext"] class TestJiraIssuesFromBoardCommand: def test_get_issues_from_board_command(self, mocker): """ Given: - A Jira client When - Calling the jira-board-issue-list command. Then - Validate that the correct context data is returned to the user. """ from JiraV3 import board_issues_list_command client = jira_base_client_mock() board_issues_raw_response = util_load_json("test_data/get_issues_from_board_test/raw_response.json") expected_command_results = util_load_json("test_data/get_issues_from_board_test/parsed_result.json") mocker.patch.object(client, "get_issues_from_board", return_value=board_issues_raw_response) command_results = board_issues_list_command(client=client, args={"board_id": "14"}) assert expected_command_results["EntryContext"] == command_results.to_context()["EntryContext"] class TestJiraBoarSprintsCommand: def test_get_issues_from_board_command(self, mocker): """ Given: - A Jira client When - Calling the jira-board-sprint-list command. Then - Validate that the correct context data is returned to the user. """ from JiraV3 import board_sprint_list_command client = jira_base_client_mock() board_sprints_raw_response = util_load_json("test_data/get_board_sprints_test/raw_response.json") expected_command_results = util_load_json("test_data/get_board_sprints_test/parsed_result.json") mocker.patch.object(client, "get_sprints_from_board", return_value=board_sprints_raw_response) command_results = board_sprint_list_command(client=client, args={"board_id": "12"}) assert expected_command_results["EntryContext"] == command_results.to_context()["EntryContext"] class TestJiraIssueLinkTypesCommand: def test_get_issue_link_types_command(self, mocker): """ Given: - A Jira client When - Calling the jira-issue-link-type-get command. Then - Validate that the correct context data is returned to the user. """ from JiraV3 import get_issue_link_types_command client = jira_base_client_mock() link_types_raw_response = util_load_json("test_data/get_issue_link_types_test/raw_response.json") expected_command_results = util_load_json("test_data/get_issue_link_types_test/parsed_result.json") mocker.patch.object(client, "get_issue_link_types", return_value=link_types_raw_response) command_results = get_issue_link_types_command(client=client, args={}) assert expected_command_results["EntryContext"] == command_results.to_context()["EntryContext"] class TestJiraIssueToIssueCommand: def test_issue_to_issue_command(self, mocker): """ Given: - A Jira client When - Calling the jira-issue-to-issue-link command. Then - Validate that the correct message is returned to the user. """ from JiraV3 import link_issue_to_issue_command client = jira_base_client_mock() mocker.patch.object(client, "create_issue_link", return_value=requests.Response()) command_results = link_issue_to_issue_command(client=client, args={}) assert command_results.to_context()["HumanReadable"] == "Issue link created successfully" class TestJiraSprintIssueMoveCommand: def test_jira_sprint_issue_move(self, mocker): """ Given: - A Jira client When - Calling the jira-sprint-issue-move command. Then - Validate that the correct message is returned to the user. """ from JiraV3 import issues_to_sprint_command client = jira_base_client_mock() mocker.patch.object(client, "issues_to_sprint", return_value=requests.Response()) command_results = issues_to_sprint_command(client=client, args={}) assert command_results.to_context()["HumanReadable"] == "Issues were moved to the Sprint successfully" class TestJiraEpicIssuesCommand: def test_get_epic_issues_command(self, mocker): """ Given: - A Jira client When - Calling the jira-epic-issue-list command. Then - Validate that the correct context data is returned to the user. """ from JiraV3 import epic_issues_list_command client = jira_base_client_mock() epic_issues_raw_response = util_load_json("test_data/get_epic_issues_test/raw_response.json") expected_command_results = util_load_json("test_data/get_epic_issues_test/parsed_result.json") mocker.patch.object(client, "get_epic_issues", return_value=epic_issues_raw_response) command_results = epic_issues_list_command(client=client, args={"epic_key": "TSTPRD-1"}) assert expected_command_results["EntryContext"] == command_results.to_context()["EntryContext"] def test_get_epic_issues_without_extracting_epic_id_command(self, mocker): """ Given: - A Jira client When - Calling the jira-epic-issue-list command, and not being able to extract the board_id from the response. Then - Validate that the context data is identified only using the epic id, without the board id. """ from JiraV3 import epic_issues_list_command client = jira_base_client_mock() epic_issues_raw_response = util_load_json("test_data/get_epic_issues_test/raw_response.json") mocker.patch.object(client, "get_epic_issues", return_value=epic_issues_raw_response) for issue in epic_issues_raw_response.get("issues", []): (issue.get("fields", {}).get("sprint", {}) or {})["originBoardId"] = "" command_results = epic_issues_list_command(client=client, args={"epic_key": "COMPANYSA-1"}) entry_context = command_results.to_context()["EntryContext"] assert "Jira.EpicIssues(val.epicId && val.epicId == obj.epicId)" in entry_context assert ( "Jira.EpicIssues(val.epicId && val.epicId == obj.epicId && val.boardId && val.boardId == obj.boardId)" ) not in entry_context class TestJiraBoardEpicsCommand: def test_get_board_epics_command(self, mocker): """ Given: - A Jira client When - Calling the jira-board-epic-list command. Then - Validate that the correct context data is returned to the user. """ from JiraV3 import board_epic_list_command client = jira_base_client_mock() board_epics_raw_response = util_load_json("test_data/get_board_epics_test/raw_response.json") expected_command_results = util_load_json("test_data/get_board_epics_test/parsed_result.json") mocker.patch.object(client, "get_epics_from_board", return_value=board_epics_raw_response) command_results = board_epic_list_command(client=client, args={"board_id": "14"}) assert expected_command_results["EntryContext"] == command_results.to_context()["EntryContext"] class TestJiraSprintIssuesCommand: def test_get_sprint_issues_command(self, mocker): """ Given: - A Jira client When - Calling the jira-sprint-issue-list command, without the board_id argument. Then - Validate that the correct API call was called to retrieve the sprint issues, which is the API call that does not require a board_id, only a sprint_id argument, and that correct context data is returned to the user. """ from JiraV3 import sprint_issues_list_command client = jira_base_client_mock() sprint_issues_raw_response = util_load_json("test_data/get_sprint_issues_test/raw_response.json") expected_command_results = util_load_json("test_data/get_sprint_issues_test/parsed_result.json") issues_from_sprint_mocker = mocker.patch.object(client, "get_issues_from_sprint", return_value=sprint_issues_raw_response) sprint_issues_from_board_mocker = mocker.patch.object( client, "get_sprint_issues_from_board", return_value=sprint_issues_raw_response ) command_results = sprint_issues_list_command(client=client, args={"sprint_id": "4"}) issues_from_sprint_mocker.assert_called_once() sprint_issues_from_board_mocker.assert_not_called() assert expected_command_results["EntryContext"] == command_results.to_context()["EntryContext"] def test_get_sprint_issues_from_board_command(self, mocker): """ Given: - A Jira client When - Calling the jira-sprint-issue-list command, with the board_id argument. Then - Validate that the correct API call was called to retrieve the sprint issues, which is the API call that does require a board_id, in addition to a sprint_id argument, and that correct context data is returned to the user. """ from JiraV3 import sprint_issues_list_command client = jira_base_client_mock() sprint_issues_raw_response = util_load_json("test_data/get_sprint_issues_test/raw_response.json") expected_command_results = util_load_json("test_data/get_sprint_issues_test/parsed_result.json") issues_from_sprint_mocker = mocker.patch.object(client, "get_issues_from_sprint", return_value=sprint_issues_raw_response) sprint_issues_from_board_mocker = mocker.patch.object( client, "get_sprint_issues_from_board", return_value=sprint_issues_raw_response ) command_results = sprint_issues_list_command(client=client, args={"board_id": "12", "sprint_id": "4"}) issues_from_sprint_mocker.assert_not_called() sprint_issues_from_board_mocker.assert_called_once() assert expected_command_results["EntryContext"] == command_results.to_context()["EntryContext"] def test_get_sprint_issues_without_extracting_board_id(self, mocker): """ Given: - A Jira client. When - Calling the jira-sprint-issue-list command, without the board_id argument, and not being able to extract the board_id from the response. Then - Validate that the context data is identified only using the sprint id, without the board id. """ from JiraV3 import sprint_issues_list_command client = jira_base_client_mock() sprint_issues_raw_response: Dict[str, Any] = util_load_json("test_data/get_sprint_issues_test/raw_response.json") issues_from_sprint_mocker = mocker.patch.object(client, "get_issues_from_sprint", return_value=sprint_issues_raw_response) sprint_issues_from_board_mocker = mocker.patch.object( client, "get_sprint_issues_from_board", return_value=sprint_issues_raw_response ) for issue in sprint_issues_raw_response.get("issues", []): (issue.get("fields", {}).get("sprint", {}) or {})["originBoardId"] = "" command_results = sprint_issues_list_command(client=client, args={"sprint_id": "4"}) issues_from_sprint_mocker.assert_called_once() sprint_issues_from_board_mocker.assert_not_called() entry_context = command_results.to_context()["EntryContext"] assert "Jira.SprintIssues(val.sprintId && val.sprintId == obj.sprintId)" in entry_context assert ( "Jira.SprintIssues(val.boardId && val.boardId == obj.boardId && val.sprintId && val.sprintId == obj.sprintId)" ) not in entry_context class TestJiraDeleteCommentCommand: def test_delete_comment_command(self, mocker): """ Given: - A Jira client. When - Calling the jira-issue-delete-comment. Then - Validate that the correct message is returned to the user. """ from JiraV3 import delete_comment_command client = jira_base_client_mock() mocker.patch.object(client, "delete_comment", return_value=requests.Response()) command_results = delete_comment_command(client=client, args={"issue_key": "dummy_issue_key"}) assert "Comment deleted successfully" in command_results.to_context()["HumanReadable"] class TestJiraGetIssueAttachmentsCommand: @pytest.mark.parametrize("args,number_of_calls", [({"attachment_id": "1,2,3"}, 3), ({"attachment_id": "1234"}, 1)]) def test_get_issue_attachments_command(self, mocker, args, number_of_calls): """ Given: - A Jira client, and attachment ids to retrieve the content of the attachments. When - Calling the jira-issue-get-attachment. Then - Validate that the number of times the function that is in charge of creating the files to display in the War Room is called with correspondence to the number of attachment ids supplied (The function that is in charge of creating the file themselves has been tested in the class TestJiraGetIssueCommand). """ from JiraV3 import issue_get_attachment_command client = jira_base_client_mock() create_file_info_mocker = mocker.patch("JiraV3.create_file_info_from_attachment", return_value={}) issue_get_attachment_command(client=client, args=args) assert len(create_file_info_mocker.mock_calls) == number_of_calls class TestJiraUploadFileCommand: def test_get_file_name_and_content(self, mocker): """ Given: - An entry id, that is found in a War Room When - Getting the file name and content of it, in order to upload to Jira Then - Validate that the function that is in charge of retrieving the name and content of the file returns the required data. """ from JiraV3 import get_file_name_and_content file_path = "test_data/get_issue_test/dummy_attachment_content.txt" expected_file_name = "dummy_attachment_content.txt" mocker.patch.object(demisto, "getFilePath", return_value={"name": expected_file_name, "path": file_path}) file_name, file_bytes = get_file_name_and_content(entry_id="dummy_entry_id") expected_file_bytes: bytes = b"" with open(file_path, "rb") as f: expected_file_bytes = f.read() assert expected_file_bytes == file_bytes assert expected_file_name == file_name def test_upload_file_command(self, mocker): """ Given: - A Jira client. When - When calling the jira-issue-upload-file command. Then - Validate that correct message is outputted to the user. """ from JiraV3 import upload_file_command client = jira_base_client_mock() upload_file_raw_response = util_load_json("test_data/upload_file_test/raw_response.json") expected_command_results_context = util_load_json("test_data/upload_file_test/parsed_result.json") mocker.patch("JiraV3.get_file_name_and_content", return_value=("dummy_file_name.pdf", b"dummy content")) mocker.patch.object(client, "upload_attachment", return_value=upload_file_raw_response) command_results = upload_file_command(client=client, args={"issue_key": "COMPANYSA-35"}) assert command_results.to_context()["HumanReadable"] == expected_command_results_context["HumanReadable"] def test_upload_XSOAR_attachment_to_jira_mime_type_check(self, mocker): """ Given: - A Jira client. When - When calling the jira-issue-upload-file command. Then - Validate that correct mime_type was given to the file. """ from JiraV3 import upload_XSOAR_attachment_to_jira client = jira_base_client_mock() file_name = "dummy_file_name.pdf" issue_key = "COMPANYSA-35" file_bytes = b"dummy content" expected_file_mime_type = "application/pdf" upload_file_raw_response = util_load_json("test_data/upload_file_test/raw_response.json") files = {"file": (file_name, file_bytes, expected_file_mime_type)} mocker.patch("JiraV3.get_file_name_and_content", return_value=("dummy_file_name.pdf", b"dummy content")) mocker.patch("JiraV3.guess_type", return_value=(expected_file_mime_type, "")) mock_request = mocker.patch.object(client, "upload_attachment", return_value=upload_file_raw_response) upload_XSOAR_attachment_to_jira(client=client, entry_id="", issue_id_or_key=issue_key) mock_request.assert_called_with(issue_id_or_key=issue_key, files=files) def test_upload_XSOAR_attachment_to_jira_mime_type_fail(self, mocker): """ Given: - A Jira client. When - When calling the jira-issue-upload-file command. Then - Validate that in case of unsuccessful upload to Jira due to mime type issue, we will try again with the default mime type. """ from JiraV3 import upload_XSOAR_attachment_to_jira client = jira_base_client_mock() issue_key = "COMPANYSA-35" mocker.patch("JiraV3.get_file_name_and_content", return_value=("dummy_file_name.pdf", b"dummy content")) mocker.patch("JiraV3.guess_type", return_value=("application/pdf", "")) mocker.patch.object(client, "upload_attachment", side_effect=DemistoException("failed to upload", res={})) mock_request = mocker.patch.object( client, "upload_attachment", side_effect=[DemistoException("failed to upload", res={}), {}] ) upload_XSOAR_attachment_to_jira(client=client, entry_id="", issue_id_or_key=issue_key) # Validate that we run upload_attachment twice, once with an error, and second time to use default file type assert mock_request.call_count == 2 # Validate that the second call uses the default file type (application-type) mock_request.assert_called_with( files={"file": ("dummy_file_name.pdf", b"dummy content", "application-type")}, issue_id_or_key=issue_key ) def test_create_files_to_upload(self, mocker): """ Given: - An empty file mime type, a file name and a file bytes. When - When calling the jira-issue-upload-file command. Then - Validate that correct mime_type was given to the file, and the object to upload is correct. """ from JiraV3 import create_files_to_upload file_name = "dummy_file_name.pdf" file_bytes = b"dummy content" expected_file_mime_type = "application/pdf" expected_files = {"file": (file_name, file_bytes, expected_file_mime_type)} mocker.patch("JiraV3.guess_type", return_value=(expected_file_mime_type, "")) result_files, result_mime_type = create_files_to_upload("", file_name, file_bytes) assert result_files == expected_files assert result_mime_type == expected_file_mime_type def test_create_files_to_upload_none_type(self, mocker): """ Given: - An empty file mime type, a file name and a file bytes. When - When calling the jira-issue-upload-file command. Then - Validate that in case of unsuccessful type guess, the default mime type is given (application-type), and the object to upload is correct. """ from JiraV3 import create_files_to_upload file_name = "dummy_file_name.pdf" file_bytes = b"dummy content" expected_file_mime_type = "application-type" expected_files = {"file": (file_name, file_bytes, expected_file_mime_type)} mocker.patch("JiraV3.guess_type", return_value=(None, "")) result_files, result_mime_type = create_files_to_upload("", file_name, file_bytes) assert result_files == expected_files assert result_mime_type == expected_file_mime_type def test_create_files_to_upload_given_type(self, mocker): """ Given: - An application-type file mime type, a file name and a file bytes. When - When calling the jira-issue-upload-file command. Then - Validate that in case of a given mime type the function guess_type wasn't called, and the object to upload is correct. """ from JiraV3 import create_files_to_upload file_name = "dummy_file_name.pdf" file_bytes = b"dummy content" expected_file_mime_type = "application-type" expected_files = {"file": (file_name, file_bytes, expected_file_mime_type)} mock_guess_type = mocker.patch("JiraV3.guess_type", return_value=(None, "")) result_files, result_mime_type = create_files_to_upload(expected_file_mime_type, file_name, file_bytes) assert result_files == expected_files assert result_mime_type == expected_file_mime_type mock_guess_type.assert_not_called() class TestJiraGetIdByAttribute: @pytest.mark.parametrize( "raw_response_path,parsed_result_path", [ ( "test_data/get_id_by_attribute_test/raw_response_cloud.json", "test_data/get_id_by_attribute_test/parsed_result_cloud.json", ), ( "test_data/get_id_by_attribute_test/raw_response_onprem.json", "test_data/get_id_by_attribute_test/parsed_result_onprem.json", ), ], ) def test_get_id_when_response_returns_one_user(self, mocker, raw_response_path, parsed_result_path): """ Given: - A Jira client When - When calling the jira-get-id-by-attribute command, and only getting one user in the response. Then - Validate that the user is returned. """ from JiraV3 import get_id_by_attribute_command client = jira_base_client_mock() user_search_raw_response = util_load_json(raw_response_path) expected_command_results_context = util_load_json(parsed_result_path) mocker.patch.object(client, "get_id_by_attribute", return_value=user_search_raw_response) command_results = get_id_by_attribute_command(client=client, args={"attribute": "fred@example.com"}) assert expected_command_results_context == command_results.to_context() @pytest.mark.parametrize( "client, raw_response_path", [ (jira_cloud_client_mock(), "test_data/get_id_by_attribute_test/raw_response_cloud.json"), (jira_onprem_client_mock(), "test_data/get_id_by_attribute_test/raw_response_onprem.json"), ], ) def test_id_not_found_when_response_returns_multiple_users(self, mocker, client, raw_response_path): """ Given: - A Jira client, once for Cloud, and once for OnPrem When - When calling the jira-get-id-by-attribute command, and getting multiple responses, and not being able to extract the account id (probably because the attribute was an email, and the email can sometimes not be returned for privacy reasons) Then - Validate that an appropriate message is returned to the user. """ from JiraV3 import get_id_by_attribute_command user_search_raw_response = util_load_json(raw_response_path) attribute = "fred@example.com" command_results_message = ( f"Multiple accounts found, but it was not possible to resolve which one" f" of them is most relevant to attribute {attribute}. Please try to provide" ' the "DisplayName" attribute if not done so before, or supply the full' " attribute." ) user_search_raw_response = user_search_raw_response * 2 # To mock that the response returned multiple users mocker.patch.object(client, "get_id_by_attribute", return_value=user_search_raw_response) command_results = get_id_by_attribute_command(client=client, args={"attribute": attribute}) assert command_results_message in command_results.to_context()["HumanReadable"] @pytest.mark.parametrize( "client, raw_response_path", [ (jira_cloud_client_mock(), "test_data/get_id_by_attribute_test/raw_response_cloud.json"), (jira_onprem_client_mock(), "test_data/get_id_by_attribute_test/raw_response_onprem.json"), ], ) def test_multiple_ids_found_when_response_returns_multiple_users(self, mocker, client, raw_response_path): """ Given: - A Jira client, once for Cloud, and once for OnPrem When - When calling the jira-get-id-by-attribute command, and getting multiple responses, and extracting multiple account ids Then - Validate that an appropriate message is returned to the user. """ from JiraV3 import get_id_by_attribute_command # client = jira_onprem_client_mock() user_search_raw_response = util_load_json(raw_response_path) attribute = "fred@example.com" command_results_message = ( f"Multiple account IDs were found for attribute: {attribute}.\n" f"Please try to provide the other attributes available - Email or DisplayName" " (and Name in the case of Jira OnPrem)." ) user_search_raw_response = user_search_raw_response * 2 # To mock that the response returned multiple users user_search_raw_response[0]["emailAddress"] = attribute mocker.patch.object(client, "get_id_by_attribute", return_value=user_search_raw_response) command_results = get_id_by_attribute_command(client=client, args={"attribute": attribute}) assert command_results_message in command_results.to_context()["HumanReadable"] @pytest.mark.parametrize( "client, raw_response_path, parsed_result_path", [ ( jira_cloud_client_mock(), "test_data/get_id_by_attribute_test/raw_response_cloud.json", "test_data/get_id_by_attribute_test/parsed_result_cloud.json", ), ( jira_onprem_client_mock(), "test_data/get_id_by_attribute_test/raw_response_onprem.json", "test_data/get_id_by_attribute_test/parsed_result_onprem.json", ), ], ) def test_get_id_from_multiple_ids_when_response_returns_multiple_users( self, mocker, client, raw_response_path, parsed_result_path ): """ Given: - A Jira client, once for Cloud, and once for OnPrem When - When calling the jira-get-id-by-attribute command, and getting multiple responses, and extracting the correct account id. Then - Validate that the user is returned. """ from JiraV3 import get_id_by_attribute_command user_search_raw_response = util_load_json(raw_response_path) user = user_search_raw_response[0] # The test data contains only one user in the raw response expected_command_results_context = util_load_json(parsed_result_path) attribute = "fred@example.com" # To mock that the response returned multiple users user_search_raw_response = [user, user.copy()] user_search_raw_response[0]["emailAddress"] = attribute user_search_raw_response[1]["emailAddress"] = "wrong attribute" mocker.patch.object(client, "get_id_by_attribute", return_value=user_search_raw_response) command_results = get_id_by_attribute_command(client=client, args={"attribute": attribute}) assert expected_command_results_context == command_results.to_context() class TestJiraGetSpecificField: def test_get_specific_field_command(self, mocker): """ Given: - A Jira client When - When calling the jira-get-specific-field Then - Validate that the specified fields are returned in the context data """ from JiraV3 import get_specific_fields_command client = jira_base_client_mock() issue_raw_response = util_load_json("test_data/get_specific_field_test/raw_response.json") expected_command_results = util_load_json("test_data/get_specific_field_test/parsed_result.json") mocker.patch.object(client, "get_issue", return_value=issue_raw_response) command_results = get_specific_fields_command(client=client, args={"issue_key": "COMPANYSA-35", "fields": "watches,rank"}) assert expected_command_results["EntryContext"] == command_results.to_context()["EntryContext"] assert expected_command_results["HumanReadable"] == command_results.to_context()["HumanReadable"] class TestJiraIssueQueryField: def test_issue_query_command(self, mocker): """ Given: - A Jira client When - When calling the jira-issue-query, with the `fields` argument Then - Validate that the context data and human readable of the queried issues are returned. """ from JiraV3 import issue_query_command client = jira_base_client_mock() issue_query_raw_response = util_load_json("test_data/get_issue_query_test/raw_response.json") expected_command_results = util_load_json("test_data/get_issue_query_test/parsed_result.json") mocker.patch.object(client, "run_query", return_value=issue_query_raw_response) command_results = issue_query_command(client=client, args={"fields": "watches,rank"}) command_results = command_results if isinstance(command_results, list) else [command_results] for expected_command_result, command_result in zip(expected_command_results, command_results): assert expected_command_result["EntryContext"] == command_result.to_context()["EntryContext"] assert expected_command_result["HumanReadable"] == command_result.to_context()["HumanReadable"] def test_issue_query_command_uses_new_endpoint_for_cloud(self, mocker, requests_mock): """ Given: - A Cloud Jira client When - When calling the jira-issue-query Then - Validate that the new endpoint is used and the command returns the expected output. """ from JiraV3 import issue_query_command client = jira_cloud_client_mock() mocker.patch.object(client, "get_headers_with_access_token", return_value={}) issue_query_raw_response = util_load_json("test_data/get_issue_query_test/raw_response.json") expected_command_results = util_load_json("test_data/get_issue_query_test/parsed_result.json") requests_mock.get("https://dummy_server_url/dummy_cloud_id/rest/api/3/search/jql", json=issue_query_raw_response) command_results = issue_query_command(client=client, args={"fields": "watches,rank"}) command_results = command_results if isinstance(command_results, list) else [command_results] for expected_command_result, command_result in zip(expected_command_results, command_results): assert expected_command_result["EntryContext"] == command_result.to_context()["EntryContext"] assert expected_command_result["HumanReadable"] == command_result.to_context()["HumanReadable"] def test_issue_query_command_uses_old_endpoint_for_onprem(self, mocker, requests_mock): """ Given: - An on-prem Jira client When - When calling the jira-issue-query Then - Validate that the old endpoint is used and the command returns the expected output. """ from JiraV3 import issue_query_command client = jira_onprem_client_mock() mocker.patch.object(client, "get_headers_with_access_token", return_value={}) issue_query_raw_response = util_load_json("test_data/get_issue_query_test/raw_response.json") expected_command_results = util_load_json("test_data/get_issue_query_test/parsed_result.json") requests_mock.get("https://dummy_server_url/rest/api/2/search", json=issue_query_raw_response) command_results = issue_query_command(client=client, args={"fields": "watches,rank"}) command_results = command_results if isinstance(command_results, list) else [command_results] for expected_command_result, command_result in zip(expected_command_results, command_results): assert expected_command_result["EntryContext"] == command_result.to_context()["EntryContext"] assert expected_command_result["HumanReadable"] == command_result.to_context()["HumanReadable"] def test_issue_query_command_with_start_at(self, mocker, requests_mock): """ Given: - A Jira client When - When calling the jira-issue-query, with the `start_at` argument Then - Validate that the old endpoint is used """ from JiraV3 import issue_query_command client = jira_cloud_client_mock() mocker.patch.object(client, "get_headers_with_access_token", return_value={}) issue_query_raw_response = util_load_json("test_data/get_issue_query_test/raw_response.json") expected_command_results = util_load_json("test_data/get_issue_query_test/parsed_result.json") requests_mock.get("https://dummy_server_url/dummy_cloud_id/rest/api/3/search", json=issue_query_raw_response) command_results = issue_query_command(client=client, args={"start_at": "10", "fields": "watches,rank"}) command_results = command_results if isinstance(command_results, list) else [command_results] for expected_command_result, command_result in zip(expected_command_results, command_results): assert expected_command_result["EntryContext"] == command_result.to_context()["EntryContext"] assert expected_command_result["HumanReadable"] == command_result.to_context()["HumanReadable"] def test_issue_query_command_with_start_at_fail(self, requests_mock): """ Given: - A Jira client When - When calling the jira-issue-query, with the `start_at` argument - The old endpoint is no longer available. Then - Validate that the old endpoint is used - Validate that the custom error message is raised for 410 Gone. """ from JiraV3 import issue_query_command client = jira_base_client_mock(username="user", api_key="key") requests_mock.get("https://dummy_url/rest/api/999/search", status_code=410) with pytest.raises(DemistoException, match="The start_at argument is no longer supported in this Jira instance."): issue_query_command(client=client, args={"start_at": "10"}) class TestJiraAddUrlLink: def test_add_url_link(self, mocker): """ Given: - A Jira client When - When calling the jira-issue-add-link Then - Validate that the correct human readable is returned to the user """ from JiraV3 import add_link_command client = jira_base_client_mock() mocker.patch.object( client, "add_link", return_value={"id": 10000, "self": "https://your-domain.atlassian.net/rest/api/issue/MKY-1/remotelink/10000"}, ) command_result = add_link_command(client=client, args={"issue_key": "dummy_issue_key"}) markdown_dict = { "id": 10000, "key": None, "comment": "", "ticket_link": "https://your-domain.atlassian.net/rest/api/issue/MKY-1/remotelink/10000", } expected_human_readable = tableToMarkdown(name="Remote Issue Link", t=markdown_dict, removeNull=True) assert command_result.to_context()["HumanReadable"] == expected_human_readable class TestJiraGetModifiedRemoteIds: USER_INFO_RES = { "accountId": "dummy_account_id", "accountType": "atlassian", "emailAddress": "admin@example.com", "displayName": "Example Example", "timeZone": "Asia/Jerusalem", "locale": "en_US", } LAST_UPDATE_TIME = "2023-05-01" def test_get_modified_issue_ids(self, mocker): """ Given: - A Jira client, the last updated time of an incident, and a timezone When - When calling get_modified_issue_ids in order to get the issues that have an updated time greater than the last updated time of the incident Then - Validate that the correct ids are returned """ from JiraV3 import get_modified_issue_ids client = jira_base_client_mock() modified_issues = {"issues": [{"id": "1234"}, {"id": "2345"}]} mocker.patch.object(client, "run_query", return_value=modified_issues) modified_issues = get_modified_issue_ids( client=client, last_update_date=self.LAST_UPDATE_TIME, timezone_name=self.USER_INFO_RES.get("timeZone", "") ) assert modified_issues == ["1234", "2345"] def test_get_modified_remote_data_command(self, mocker): """ Given: - A Jira client When - When calling the mirroring mechanism the command get_modified_remote_data_command is called in order to retrieve the Jira issues that have been updated since the last update time of the incident. Then - Validate that the correct ids are returned """ from JiraV3 import get_modified_remote_data_command client = jira_base_client_mock() mocker.patch("JiraV3.get_modified_issue_ids", return_value=["1234", "2345"]) mocker.patch.object(client, "get_user_info", return_value=self.USER_INFO_RES) get_modified_remote_data = get_modified_remote_data_command(client=client, args={"lastUpdate": self.LAST_UPDATE_TIME}) assert get_modified_remote_data.modified_incident_ids == ["1234", "2345"] class TestJiraGetMappingFields: ISSUE_FIELDS_RES = [ {"id": "statuscategorychangedate", "key": "statuscategorychangedate", "name": "Status Category Changed"}, {"id": "parent", "key": "parent", "name": "Parent"}, ] def test_get_mapping_fields_command(self, mocker): """ Given: - A Jira client When - When calling get-mapping-fields command. Then - Validate that we are able to extract the ids from the response from the API. """ from JiraV3 import get_mapping_fields_command client = jira_base_client_mock() mocker.patch.object(client, "get_issue_fields", return_value=self.ISSUE_FIELDS_RES) mapping_fields = get_mapping_fields_command(client=client) assert list(mapping_fields.scheme_types_mappings[0].fields.keys()) == [ "statuscategorychangedate", "parent", "issue_id", "summary", "description", "labels", "components", "priority", "due_date", "assignee", "status", "assignee_id", "original_estimate", ] class TestJiraUpdateRemoteSystem: def test_update_remote_system_using_delta(self, mocker): """ Given: - A Jira client When - When the mirror out mechanism is called, which calls the update-remote-system command, and we need to edit the issue Then - Validate that the API call to edit the issue was called with the correct data, which was extracted from the delta and data arguments. """ from JiraV3 import update_remote_system_command client = jira_base_client_mock() args = { "incidentChanged": "17757", "remoteId": "17757", "data": {"summary": "data", "not_changes_key": "not_changes_val"}, "delta": {"summary": "changes", "dbotMirrorDirection": "test"}, } edit_issue_mocker = mocker.patch.object(client, "edit_issue", return_value=requests.Response()) update_remote_system_res = update_remote_system_command( client=client, args=args, comment_tag_to_jira="", attachment_tag_to_jira="" ) assert update_remote_system_res == "17757" assert edit_issue_mocker.call_args[1]["json_data"] == {"fields": {"summary": "data"}} def test_update_remote_system_using_file_entry_with_correct_tag(self, mocker): """ Given: - A Jira client When - When the mirror out mechanism is called, which calls the update-remote-system command, and we get an attachment with the appropriate attachment tag. Then - Validate that the attachment gets uploaded to Jira, and that the name of the file contains the constant ATTACHMENT_MIRRORED_FROM_XSOAR """ from JiraV3 import ATTACHMENT_MIRRORED_FROM_XSOAR, update_remote_system_command client = jira_base_client_mock() args = { "remoteId": "17757", "entries": [{"entry_id": "1234", "type": EntryType.FILE, "tags": ["attachment_tag"]}], } upload_attachment_mocker = mocker.patch("JiraV3.upload_XSOAR_attachment_to_jira", return_value=[]) update_remote_system_res = update_remote_system_command( client=client, args=args, comment_tag_to_jira="", attachment_tag_to_jira="attachment_tag" ) assert update_remote_system_res == "17757" assert ATTACHMENT_MIRRORED_FROM_XSOAR in upload_attachment_mocker.call_args[1]["attachment_name"] @pytest.mark.parametrize("client", [(jira_cloud_client_mock()), (jira_onprem_client_mock())]) def test_update_remote_system_using_entry_with_correct_comment_tag(self, mocker, client): """ Given: - A Jira client When - When the mirror out mechanism is called, which calls the update-remote-system command, and we get a comment with the appropriate comment tag. Then - Validate that the comment gets uploaded to Jira, and the content of the comment contains the constant COMMENT_MIRRORED_FROM_XSOAR """ from JiraV3 import COMMENT_MIRRORED_FROM_XSOAR, update_remote_system_command args = { "remoteId": "17757", "entries": [{"entry_id": "1234", "tags": ["comment_tag"], "contents": "some comment"}], } add_comment_mocker = mocker.patch.object(client, "add_comment", return_value={}) update_remote_system_res = update_remote_system_command( client=client, args=args, comment_tag_to_jira="comment_tag", attachment_tag_to_jira="" ) assert update_remote_system_res == "17757" assert COMMENT_MIRRORED_FROM_XSOAR in str(add_comment_mocker.call_args[1]["json_data"]) class TestJiraGetRemoteData: def test_entries_returned_when_configured_not_to_return(self, mocker): """ Given: - A Jira client When - When the mirror in mechanism is called, which calls the get-remote-data command, and the user configured not to return comments nor attachments Then - Validate that no entries are returned """ from JiraV3 import ATTACHMENT_MIRRORED_FROM_XSOAR, get_updated_remote_data attachments_entries = [ {"File": "dummy_file_name", "FileID": "id1"}, {"File": f"dummy_file_name{ATTACHMENT_MIRRORED_FROM_XSOAR}", "FileID": "id2"}, ] comments_entries = [ {"Comment": "Comment 1", "Updated": "2023-01-01", "UpdatedUser": "User 1"}, {"Comment": "Comment 2", "Updated": "2023-05-01", "UpdatedUser": "User 2"}, {"Comment": "Comment 3", "Updated": "2023-05-01", "UpdatedUser": "User 3"}, ] client = jira_base_client_mock() mocker.patch("JiraV3.get_attachments_entries_for_fetched_incident", return_value=attachments_entries) mocker.patch("JiraV3.get_comments_entries_for_fetched_incident", return_value=comments_entries) updated_incident: Dict[str, Any] = {} parsed_entries = get_updated_remote_data( client=client, issue={}, updated_incident=updated_incident, issue_id="1234", mirror_resolved_issue=False, attachment_tag_from_jira="attachment from jira", comment_tag_from_jira="", user_timezone_name="", incident_modified_date=None, fetch_comments=False, fetch_attachments=False, ) assert parsed_entries == [] def test_get_attachment_entries(self, mocker): """ Given: - A Jira client When - When the mirror in mechanism is called, which calls the get-remote-data command, and we want to extract the attachments of the Jira issue Then - Validate that the attachments get added to the appropriate incident field, and only the entries that don't contain the constant ATTACHMENT_MIRRORED_FROM_XSOAR in their names, get the tag added to them. """ from JiraV3 import ATTACHMENT_MIRRORED_FROM_XSOAR, get_updated_remote_data attachments_entries = [ {"File": "dummy_file_name_old", "FileID": "id1", "created": "2024-01-01T00:00:00.000+0300"}, {"File": "dummy_file_name", "FileID": "id1", "created": "2024-02-01T00:00:00.000+0300"}, { "File": f"dummy_file_name{ATTACHMENT_MIRRORED_FROM_XSOAR}", "FileID": "id2", "created": "2024-02-01T00:00:00.000+0300", }, ] create_file_mock_res = [{k: v for k, v in item.items() if k != "created"} for item in attachments_entries[1:]] client = jira_base_client_mock() mocker.patch("JiraV3.create_file_info_from_attachment", side_effect=create_file_mock_res) mocker.patch("demistomock.get", return_value=attachments_entries) mocker.patch("JiraV3.get_comments_entries_for_fetched_incident", return_value=[]) updated_incident: Dict[str, Any] = {} user_timezone = "Asia/Jerusalem" parsed_entries = get_updated_remote_data( client=client, issue={}, updated_incident=updated_incident, issue_id="1234", mirror_resolved_issue=False, attachment_tag_from_jira="attachment from jira", comment_tag_from_jira="", user_timezone_name=user_timezone, incident_modified_date=arg_to_datetime("2024-01-01T00:00:00.000+0300"), fetch_comments=False, fetch_attachments=True, ) expected_extracted_attachments = [ {"path": "id1", "name": "dummy_file_name"}, {"path": "id2", "name": "dummy_file_name_mirrored_from_xsoar"}, ] expected_parsed_entries = [{"File": "dummy_file_name", "FileID": "id1", "Tags": ["attachment from jira"]}] assert updated_incident.get("extractedAttachments") == expected_extracted_attachments assert parsed_entries == expected_parsed_entries def test_get_comment_entries(self, mocker): """ Given: - A Jira client When - When the mirror in mechanism is called, which calls the get-remote-data command, and we want to extract the comments of the Jira issue Then - Validate that the comments get added to the appropriate incident field, and only the entries that don't contain the constant ATTACHMENT_MIRRORED_FROM_XSOAR in their names, and have an updated time that is greater than the last incident update get the tag added to them """ from JiraV3 import COMMENT_MIRRORED_FROM_XSOAR, get_updated_remote_data comments_entries = [ {"Comment": f"Comment 1 {COMMENT_MIRRORED_FROM_XSOAR}", "Updated": "2023-01-01", "UpdatedUser": "User 1"}, {"Comment": f"Comment 2 {COMMENT_MIRRORED_FROM_XSOAR}", "Updated": "2023-05-01", "UpdatedUser": "User 2"}, {"Comment": "Comment 3", "Updated": "2023-05-01", "UpdatedUser": "User 3"}, ] client = jira_base_client_mock() mocker.patch("JiraV3.get_comments_entries_for_fetched_incident", return_value=comments_entries) mocker.patch("JiraV3.get_attachments_entries_for_fetched_incident", return_value=[]) user_timezone = "Asia/Jerusalem" dateparser_parse_mocker = mocker.patch("JiraV3.dateparser.parse", side_effect=dateparser.parse) updated_incident: Dict[str, Any] = {} parsed_entries = get_updated_remote_data( client=client, issue={}, updated_incident=updated_incident, issue_id="1234", mirror_resolved_issue=False, attachment_tag_from_jira="", comment_tag_from_jira="comment from jira", user_timezone_name=user_timezone, incident_modified_date=dateparser.parse("2023-04-01"), fetch_comments=True, fetch_attachments=False, ) expected_extracted_attachments = [ {"Comment": f"Comment 1 {COMMENT_MIRRORED_FROM_XSOAR}", "Updated": "2023-01-01", "UpdatedUser": "User 1"}, {"Comment": f"Comment 2 {COMMENT_MIRRORED_FROM_XSOAR}", "Updated": "2023-05-01", "UpdatedUser": "User 2"}, {"Comment": "Comment 3", "Updated": "2023-05-01", "UpdatedUser": "User 3"}, ] expected_parsed_entries = [ { "Type": 1, "Contents": "Comment 3\nJira Author: None", "ContentsFormat": "text", "Tags": ["comment from jira"], "Note": True, } ] assert updated_incident.get("extractedComments") == expected_extracted_attachments assert parsed_entries == expected_parsed_entries assert dateparser_parse_mocker.call_args[1]["settings"]["TIMEZONE"] == user_timezone @pytest.mark.parametrize( "issue, should_be_closed", [ ({"id": "1234", "fields": {"status": {"name": "Done"}, "resolutiondate": ""}}, True), ({"id": "1234", "fields": {"status": {"name": "Fixed"}, "resolutiondate": "2023-01-01"}}, True), ({"id": "1234", "fields": {"status": {"name": "Fixed"}, "resolutiondate": ""}}, False), ], ) def test_close_incident_entry(self, mocker, issue, should_be_closed): """ Given: - A Jira client When - When the mirror in mechanism is called, which calls the get-remote-data command, and the remote Jira issue has been marked as resolved, or status has been changed to Done Then - Validate that correct entry is returned, which contains data about closing the incident in XSOAR """ from JiraV3 import get_updated_remote_data client = jira_base_client_mock() mocker.patch("JiraV3.get_comments_entries_for_fetched_incident", return_value=[]) mocker.patch("JiraV3.get_attachments_entries_for_fetched_incident", return_value=[]) parsed_entries = get_updated_remote_data( client=client, issue=issue, updated_incident=issue, issue_id="1234", mirror_resolved_issue=True, attachment_tag_from_jira="", comment_tag_from_jira=" from jira", user_timezone_name="", incident_modified_date=None, fetch_comments=False, fetch_attachments=False, ) if should_be_closed: close_reason = 'Issue was marked as "Resolved", or status was changed to "Done"' closed_entry = [ {"Type": 1, "Contents": {"dbotIncidentClose": True, "closeReason": close_reason}, "ContentsFormat": "json"} ] assert parsed_entries == closed_entry else: assert parsed_entries == [] def test_get_remote_data_response_is_returned(self, mocker): """ Given: - A Jira client When - When the mirror in mechanism is called, which calls the get-remote-data command Then - Validate that correct entries are indeed returned """ from JiraV3 import get_remote_data_command client = jira_base_client_mock() issue_response = {"id": "1234", "fields": {"summary": "dummy summary", "updated": "2023-01-01"}} mocker.patch.object(client, "get_issue", return_value=issue_response) mocker.patch("JiraV3.get_cached_user_timezone", return_value="Asia/Jerusalem") close_reason = 'Issue was marked as "Resolved", or status was changed to "Done"' expected_parsed_entries = [ { "Type": 1, "Contents": "Comment 3\nJira Author: None", "ContentsFormat": "text", "Tags": ["comment from jira"], "Note": True, }, {"File": "dummy_file_name", "FileID": "id1", "Tags": ["attachment from jira"]}, {"Type": 1, "Contents": {"dbotIncidentClose": True, "closeReason": close_reason}, "ContentsFormat": "json"}, ] mocker.patch("JiraV3.get_updated_remote_data", return_value=expected_parsed_entries) remote_data_response = get_remote_data_command( client=client, args={"id": "1234", "lastUpdate": "2023-01-01"}, attachment_tag_from_jira="", comment_tag_from_jira="", mirror_resolved_issue=True, fetch_comments=True, fetch_attachments=True, ) assert remote_data_response.entries == expected_parsed_entries class TestJiraFetchIncidents: FETCH_INCIDENTS_QUERY_CASES = [ ( "id", "status!=done", "1234", "2023-05-01", "2023-05-02", "2023-02-01", [1, 2, 3, 4], "status!=done AND id >= 1234 AND ID NOT IN (1, 2, 3, 4) ORDER BY id ASC", ), ( "id", "status!=done", "1234", "2023-05-01", "2023-05-02", "2023-02-01", [], "status!=done AND id >= 1234 ORDER BY id ASC", ), ( "created date", "status!=done", "1234", "2023-05-01", "2023-05-02", "2023-02-01", [1, 2, 3, 4], 'status!=done AND created >= "2023-05-01" AND ID NOT IN (1, 2, 3, 4) ORDER BY created ASC', ), ( "created date", "status!=done", "1234", "2023-05-01", "2023-05-02", "2023-02-01", [], 'status!=done AND created >= "2023-05-01" ORDER BY created ASC', ), ( "created date", "status!=done", "1234", "", "2023-05-02", "2023-02-01", [], 'status!=done AND created >= "2023-02-01" ORDER BY created ASC', ), ( "created date", "status!=done", "1234", "", "2023-05-02", "2023-02-01", [1, 2, 3, 4], 'status!=done AND created >= "2023-02-01" AND ID NOT IN (1, 2, 3, 4) ORDER BY created ASC', ), ( "updated date", "status!=done", "1234", "2023-05-01", "2023-05-02", "2023-02-01", [1, 2, 3, 4], 'status!=done AND updated >= "2023-05-02" AND ID NOT IN (1, 2, 3, 4) ORDER BY updated ASC', ), ( "updated date", "status!=done", "1234", "2023-05-01", "2023-05-02", "2023-02-01", [], 'status!=done AND updated >= "2023-05-02" ORDER BY updated ASC', ), ( "updated date", "status!=done", "1234", "2023-05-01", "", "2023-02-01", [], 'status!=done AND updated >= "2023-02-01" ORDER BY updated ASC', ), ( "updated date", "status!=done", "1234", "2023-05-01", "", "2023-02-01", [1, 2, 3, 4], 'status!=done AND updated >= "2023-02-01" AND ID NOT IN (1, 2, 3, 4) ORDER BY updated ASC', ), ( "id", '"request type" = "Report a Potential Security Incident"', "0", "", "", "", [], '"request type" = "Report a Potential Security Incident" AND id >= 0 ORDER BY id ASC', ), ] @pytest.mark.parametrize( ( "issue_field_to_fetch_from, fetch_query, last_fetch_id," "last_fetch_created_time, last_fetch_updated_time," "first_fetch_interval, issue_ids_to_exclude, expected_fetch_query" ), FETCH_INCIDENTS_QUERY_CASES, ) def test_create_fetch_incidents_query( self, issue_field_to_fetch_from: str, fetch_query: str, last_fetch_id: int, last_fetch_created_time: str, last_fetch_updated_time: str, first_fetch_interval: str, issue_ids_to_exclude: List[int], expected_fetch_query: str, ): """ Given: - Arguments to create the fetch query, which are: 1. issue_field_to_fetch_from: the issue field to fetch by 2. fetch_query: the query to include in every fetch 3. last_fetch_id: the last fetched id 4. last_fetch_created_time: the created time of the last fetch issue 5. last_fetch_updated_time: the updated time of the last fetch issue 6. first_fetch_interval: the first fetch interval to fetch from if the fetch timestamp is empty, and we are fetching using created or updated time. 7. issue_ids_to_exclude: the ids of the issues that we want to exclude. When - When fetching incidents Then - Validate that the correct fetch query is created given the above arguments """ from JiraV3 import create_fetch_incidents_query fetch_query = create_fetch_incidents_query( issue_field_to_fetch_from, fetch_query, last_fetch_id, last_fetch_created_time, last_fetch_updated_time, first_fetch_interval, issue_ids_to_exclude, ) assert fetch_query == expected_fetch_query def test_get_comments_entries_for_fetched_incident(self, mocker): """ Given: - A Jira client, and an issue id or key When - When extracting the issue's comments as entries to put in the incident field Then - Validate that the correct data is extracted and returned """ from JiraV3 import get_comments_entries_for_fetched_incident client = jira_base_client_mock() comments_raw_response = util_load_json("test_data/get_comments_test/raw_response.json") mocker.patch.object(client, "get_comments", return_value=comments_raw_response) comments_entries = get_comments_entries_for_fetched_incident(client=client, issue_id_or_key="1234") expected_comments_entries = [ { "Id": "18322", "Comment": "Hello there", "User": "Example User", "Created": "2023-03-23T07:45:29.056+0200", "Updated": "2023-03-23T07:45:29.056+0200", "UpdateUser": "Example User", }, { "Id": "18329", "Comment": "Second comment", "User": "Example User", "Created": "2023-03-27T20:54:15.878+0300", "Updated": "2023-03-27T20:54:15.878+0300", "UpdateUser": "Example User", }, { "Id": "18394", "Comment": "This is a comment from Jira demo", "User": "Example User", "Created": "2023-04-24T15:41:54.472+0300", "Updated": "2023-04-24T15:41:54.472+0300", "UpdateUser": "Example User", }, ] assert expected_comments_entries == comments_entries def test_get_attachments_entries_for_fetched_incident(self, mocker): """ Given: - A Jira client, and an issue id or key When - When extracting the issue's attachments as entries to put in the incident field Then - Validate that the correct data is extracted and returned """ from JiraV3 import get_attachments_entries_for_fetched_incident client = jira_base_client_mock() attachment_metadata_raw_response = util_load_json("test_data/get_issue_test/raw_response_attachment_metadata.json") expected_attachments_entries = [ { "Contents": "", "ContentsFormat": "dummy_format", "Type": "dummy_type", "File": "dummy_filename_1", "FileID": "dummy_id_1", }, { "Contents": "", "ContentsFormat": "dummy_format", "Type": "dummy_type", "File": "dummy_filename_2", "FileID": "dummy_id_2", }, ] mocker.patch("JiraV3.create_file_info_from_attachment", side_effect=expected_attachments_entries) attachments_entries = get_attachments_entries_for_fetched_incident( client=client, attachments_metadata=[attachment_metadata_raw_response, attachment_metadata_raw_response] ) assert expected_attachments_entries == attachments_entries def test_get_fetched_attachments(self, mocker): """ Given: - A Jira client, and the issue response When - Extracting the attachments of the fetched issue Then - Validate that the correct data is extracted and returned """ from JiraV3 import get_fetched_attachments attachments_entries = [ { "Contents": "", "ContentsFormat": "dummy_format", "Type": "dummy_type", "File": "dummy_filename_1", "FileID": "dummy_id_1", }, { "Contents": "", "ContentsFormat": "dummy_format", "Type": EntryType.ERROR, "File": "dummy_filename_2", "FileID": "dummy_id_2", }, ] mocker.patch("JiraV3.get_attachments_entries_for_fetched_incident", return_value=attachments_entries) client = jira_base_client_mock() expected_fetched_attachments = [{"path": "dummy_id_1", "name": "dummy_filename_1"}] fetched_attachments = get_fetched_attachments(client=client, issue={}) assert expected_fetched_attachments == fetched_attachments def test_get_fetched_comments(self, mocker): """ Given: - A Jira client, and an issue id. When - Extracting the comments' entries of the fetched issue. Then - Validate that the correct data is extracted and returned. """ from JiraV3 import get_fetched_comments expected_comments_entries = [ { "Id": "18322", "Comment": "Hello there", "User": "Example User", "Created": "2023-03-23T07:45:29.056+0200", "Updated": "2023-03-23T07:45:29.056+0200", "UpdateUser": "Example User", }, { "Id": "18329", "Comment": "Second comment", "User": "Example User", "Created": "2023-03-27T20:54:15.878+0300", "Updated": "2023-03-27T20:54:15.878+0300", "UpdateUser": "Example User", }, ] mocker.patch("JiraV3.get_comments_entries_for_fetched_incident", return_value=expected_comments_entries) attachments_entries = [ { "Contents": "", "ContentsFormat": "dummy_format", "Type": "dummy_type", "File": "dummy_filename_1", "FileID": "dummy_id_1", }, { "Contents": "", "ContentsFormat": "dummy_format", "Type": EntryType.ERROR, "File": "dummy_filename_2", "FileID": "dummy_id_2", }, ] mocker.patch("JiraV3.get_attachments_entries_for_fetched_incident", return_value=attachments_entries) client = jira_base_client_mock() comments_entries = get_fetched_comments(client=client, issue_id="1234") assert comments_entries == expected_comments_entries def test_add_extracted_data_to_incident(self): """ Given: - An issue response When - Extracting data from the issue raw response, to insert it to the respective incident fields Then - Validate that the correct data is extracted and returned """ from JiraV3 import add_extracted_data_to_incident issue = util_load_json("test_data/get_issue_test/raw_response.json") expected_issue = add_extracted_data_to_incident(issue=issue) expected_extracted_issue_data = { "extractedSubtasks": [{"id": "21525", "key": "COMPANYSA-63"}, {"id": "21538", "key": "COMPANYSA-70"}], "extractedCreator": "Example User(admin@test.com)", "extractedComponents": ["Almost-Done", "dummy-comp", "Integration", "New-Component"], } assert expected_extracted_issue_data.items() <= expected_issue.items() @pytest.mark.parametrize( "issue_field_priority, severity", [ ({"name": "Highest"}, 4), ({"name": "High"}, 3), ({"name": "Medium"}, 2), ({"name": "Low"}, 1), ({"name": "Lowest"}, 1), ({"name": "Extreme"}, 0), ], ) def test_get_jira_issue_severity(self, issue_field_priority, severity): """ Given: - The priority field of an issue When - Determining the severity of the incident Then - Validate that the priority of the issue is mapped to the correct severity """ from JiraV3 import get_jira_issue_severity assert severity == get_jira_issue_severity(issue_field_priority) def test_parse_custom_fields(self): """ Given: - An issue response When - Parsing the custom fields in the response to a more human readable form Then - Validate that the data of the custom fields get parsed and show the correct data """ from JiraV3 import parse_custom_fields issue = util_load_json("test_data/get_issue_test/raw_response.json") expected_parsed_custom_fields = util_load_json("test_data/parsed_issue_custom_fields.json") parse_custom_fields(issue=issue, issue_fields_id_to_name_mapping=issue.get("names", {}) or {}) assert expected_parsed_custom_fields == issue @pytest.mark.parametrize( "dateparser_settings, expected_parsed_created_time, expected_parsed_updated_time", [ pytest.param(None, "2025-03-14 06:54", "2025-03-14 07:59", id="No timezone conversion"), pytest.param({"TIMEZONE": "UTC-4"}, "2025-03-14 09:54", "2025-03-14 10:59", id="Convert to EST"), pytest.param({"TIMEZONE": "UTC+3"}, "2025-03-14 16:54", "2025-03-14 17:59", id="Convert to IDT"), ], ) def test_parse_issue_times_for_next_run( self, dateparser_settings: dict | None, expected_parsed_created_time: str, expected_parsed_updated_time: str, ): """ Given: - Issue created and updated timestamps in the UTC-7 timezone (US West Coast) When - Parsing the issue timestamps for fetch incidents next run Then - Validate that the timestamp fields get parsed correctly according to the date parser settings. """ from JiraV3 import parse_issue_times_for_next_run parsed_created_time, parsed_updated_time = parse_issue_times_for_next_run( issue_id=1234, issue_created_time="2025-03-14T06:54:33.000-0700", issue_updated_time="2025-03-14T07:59:09.000-0700", dateparser_settings=dateparser_settings, ) assert parsed_created_time == expected_parsed_created_time assert parsed_updated_time == expected_parsed_updated_time def test_set_last_run_when_first_time_running_fetch(self, mocker): """ Given: - Arguments to use when calling the fetch incidents mechanism When - Calling the fetch incidents mechanism for the first time (last_run is empty) Then - Validate that the last run object gets saved with the correct data """ from JiraV3 import DEFAULT_FETCH_LIMIT, fetch_incidents client = jira_base_client_mock() mocker.patch("JiraV3.demisto.getLastRun", return_value={}) # empty last run (first fetch) -> Convert timezone get_user_timezone_mocker = mocker.patch("JiraV3.get_cached_user_timezone", return_value="UTC") mocker.patch("JiraV3.create_incident_from_issue", return_value={}) set_last_run_mocker = mocker.patch("JiraV3.demisto.setLastRun") query_raw_response = { "issues": [ {"id": "1", "fields": {"created": "2023-12-11 21:09", "updated": "2023-12-12 21:09"}}, {"id": "2", "fields": {"created": "2023-12-11 22:09", "updated": "2023-12-12 22:09"}}, ] } mocker.patch.object(client, "run_query", return_value=query_raw_response) fetch_incidents( client=client, issue_field_to_fetch_from="created date", fetch_query="status!=done", id_offset=1234, fetch_attachments=True, fetch_comments=True, max_fetch_incidents=DEFAULT_FETCH_LIMIT, first_fetch_interval="3 days", mirror_direction="Incoming And Outgoing", comment_tag_to_jira="comment_tag_to_jira", comment_tag_from_jira="comment_tag_from_jira", attachment_tag_to_jira="attachment_tag_to_jira", attachment_tag_from_jira="attachment_tag_from_jira", ) expected_last_run = { "issue_ids": [1, 2], "id": 2, "created_date": "2023-12-11 22:09", "updated_date": "2023-12-12 22:09", "convert_timezone": True, # last run was empty, so timezone was converted } assert expected_last_run == set_last_run_mocker.call_args[0][0] assert get_user_timezone_mocker.call_count == 1 def test_set_last_run_when_timezone_has_utc_offset(self, mocker): """ Given: - Arguments to use when calling the fetch incidents mechanism When - Calling the fetch incidents mechanism with `convert_timezone` set to True Then - Validate that the last run object gets saved with the correct data """ from JiraV3 import DEFAULT_FETCH_LIMIT, fetch_incidents client = jira_base_client_mock() mocker.patch( "JiraV3.demisto.getLastRun", return_value={ "issue_ids": [1], "id": 1, "created_date": "2023-12-11 21:04", "updated_date": "2023-12-12 22:08", "convert_timezone": True, }, ) get_user_timezone_mocker = mocker.patch("JiraV3.get_cached_user_timezone", return_value="UTC-4") mocker.patch("JiraV3.create_incident_from_issue", return_value={}) set_last_run_mocker = mocker.patch("JiraV3.demisto.setLastRun") query_raw_response = { "issues": [ {"id": "2", "fields": {"created": "2023-12-11 21:09-0700", "updated": "2023-12-12 21:09-0700"}}, {"id": "3", "fields": {"created": "2023-12-11 22:09-0700", "updated": "2023-12-12 23:18-0700"}}, ] } mocker.patch.object(client, "run_query", return_value=query_raw_response) fetch_incidents( client=client, issue_field_to_fetch_from="created date", fetch_query="status!=done", id_offset=1234, fetch_attachments=True, fetch_comments=True, max_fetch_incidents=DEFAULT_FETCH_LIMIT, first_fetch_interval="3 days", mirror_direction="Incoming And Outgoing", comment_tag_to_jira="comment_tag_to_jira", comment_tag_from_jira="comment_tag_from_jira", attachment_tag_to_jira="attachment_tag_to_jira", attachment_tag_from_jira="attachment_tag_from_jira", ) expected_last_run = { "issue_ids": [2, 3], "id": 3, "created_date": "2023-12-12 01:09", # converted last created and updated from UTC-7 to UTC-4 "updated_date": "2023-12-13 02:18", "convert_timezone": True, # last run was empty, so timezone was converted } assert expected_last_run == set_last_run_mocker.call_args[0][0] assert get_user_timezone_mocker.call_count == 1 def test_set_last_run_when_last_run_is_not_empty(self, mocker): """ Given: - Arguments to use when calling the fetch incidents mechanism When - Calling the fetch incidents mechanism, and the last_run object is not empty Then - Validate that the last run object gets saved with the correct data """ from JiraV3 import DEFAULT_FETCH_LIMIT, fetch_incidents client = jira_base_client_mock() mocker.patch( # convert_timezone is False -> No timezone conversion "JiraV3.demisto.getLastRun", return_value={ "issue_ids": ["1", "2"], "id": "2", "created_date": "2023-12-11 22:09", "updated_date": "2023-12-12 22:09", "convert_timezone": False, }, ) mocker.patch("JiraV3.create_incident_from_issue", return_value={}) set_last_run_mocker = mocker.patch("JiraV3.demisto.setLastRun") query_raw_response = { "issues": [ {"id": "3", "fields": {"created": "2024-01-11 21:09", "updated": "2024-01-12 21:09"}}, {"id": "4", "fields": {"created": "2024-01-11 22:09", "updated": "2024-01-12 22:09"}}, ] } mocker.patch.object(client, "run_query", return_value=query_raw_response) fetch_incidents( client=client, issue_field_to_fetch_from="created date", fetch_query="status!=done", id_offset=1234, fetch_attachments=True, fetch_comments=True, max_fetch_incidents=DEFAULT_FETCH_LIMIT, first_fetch_interval="3 days", mirror_direction="Incoming And Outgoing", comment_tag_to_jira="comment_tag_to_jira", comment_tag_from_jira="comment_tag_from_jira", attachment_tag_to_jira="attachment_tag_to_jira", attachment_tag_from_jira="attachment_tag_from_jira", ) expected_last_run = { "issue_ids": [3, 4], "id": 4, "created_date": "2024-01-11 22:09", "updated_date": "2024-01-12 22:09", "convert_timezone": False, # last run was not empty, no timezone conversion to keep backwards compatibility } assert expected_last_run == set_last_run_mocker.call_args[0][0] def test_set_last_run_when_we_did_not_progress_in_created_time(self, mocker): """ Given: - Arguments to use when calling the fetch incidents mechanism When - Fetching incidents by the created date field, and we did no progress in terms of time (the created time stayed the same as the last fetch) Then - Validate that the issue ids from the last run get also added as part of the last_run object, since we did not progress in time """ from JiraV3 import DEFAULT_FETCH_LIMIT, fetch_incidents client = jira_base_client_mock() mocker.patch( # last run not empty -> No timezone conversion "JiraV3.demisto.getLastRun", return_value={ "issue_ids": ["1", "2"], "id": "2", "created_date": "2023-12-11 22:09", "updated_date": "2023-12-12 22:09", }, ) mocker.patch("JiraV3.create_incident_from_issue", return_value={}) set_last_run_mocker = mocker.patch("JiraV3.demisto.setLastRun") query_raw_response = { "issues": [ {"id": "3", "fields": {"created": "2023-12-11 22:09", "updated": "2024-01-12 21:09"}}, {"id": "4", "fields": {"created": "2023-12-11 22:09", "updated": "2024-01-12 22:09"}}, ] } mocker.patch.object(client, "run_query", return_value=query_raw_response) fetch_incidents( client=client, issue_field_to_fetch_from="created date", fetch_query="status!=done", id_offset=1234, fetch_attachments=True, fetch_comments=True, max_fetch_incidents=DEFAULT_FETCH_LIMIT, first_fetch_interval="3 days", mirror_direction="Incoming And Outgoing", comment_tag_to_jira="comment_tag_to_jira", comment_tag_from_jira="comment_tag_from_jira", attachment_tag_to_jira="attachment_tag_to_jira", attachment_tag_from_jira="attachment_tag_from_jira", ) expected_last_run = { "issue_ids": [3, 4, 1, 2], "id": 4, "created_date": "2023-12-11 22:09", "updated_date": "2024-01-12 22:09", "convert_timezone": False, # last run was not empty, no timezone conversion to keep backwards compatibility } assert expected_last_run == set_last_run_mocker.call_args[0][0] def test_set_last_run_when_we_did_not_progress_in_updated_time(self, mocker): """ Given: - Arguments to use when calling the fetch incidents mechanism When - Fetching incidents by the updated date field, and we did no progress in terms of time (the created time stayed the same as the last fetch) Then - Validate that the issue ids from the last run get also added as part of the last_run object, since we did not progress in time """ from JiraV3 import DEFAULT_FETCH_LIMIT, fetch_incidents client = jira_base_client_mock() mocker.patch( # last run not empty -> No timezone conversion "JiraV3.demisto.getLastRun", return_value={ "issue_ids": ["1", "2"], "id": "2", "created_date": "2023-12-11 22:09", "updated_date": "2023-12-12 22:09", }, ) mocker.patch("JiraV3.create_incident_from_issue", return_value={}) set_last_run_mocker = mocker.patch("JiraV3.demisto.setLastRun") query_raw_response = { "issues": [ {"id": "3", "fields": {"created": "2022-01-12 22:09", "updated": "2023-12-12 22:09"}}, {"id": "4", "fields": {"created": "2022-01-11 22:09", "updated": "2023-12-12 22:09"}}, ] } mocker.patch.object(client, "run_query", return_value=query_raw_response) fetch_incidents( client=client, issue_field_to_fetch_from="updated date", fetch_query="status!=done", id_offset=1234, fetch_attachments=True, fetch_comments=True, max_fetch_incidents=DEFAULT_FETCH_LIMIT, first_fetch_interval="3 days", mirror_direction="Incoming And Outgoing", comment_tag_to_jira="comment_tag_to_jira", comment_tag_from_jira="comment_tag_from_jira", attachment_tag_to_jira="attachment_tag_to_jira", attachment_tag_from_jira="attachment_tag_from_jira", ) expected_last_run = { "issue_ids": [3, 4, 1, 2], "id": 4, "created_date": "2022-01-11 22:09", "updated_date": "2023-12-12 22:09", "convert_timezone": False, # last run was not empty, no timezone conversion to keep backwards compatibility } assert expected_last_run == set_last_run_mocker.call_args[0][0] def test_create_incident_from_issue(self, mocker): """ Given: - Arguments to use when calling the fetch incidents mechanism When - Fetching incidents (in this unit test, we fetch two incidents, but only check on the first incident) Then - Validate that the correct value of the rawJSON key is returned, which will be used for the incident fields """ from JiraV3 import fetch_incidents query_raw_response = util_load_json("test_data/issue_query_test/raw_response.json") issue_incident = util_load_json("test_data/fetch_incidents_test/issue_incident.json") client = jira_base_client_mock() mocker.patch.object(client, "run_query", return_value=query_raw_response) mocker.patch("JiraV3.get_fetched_attachments", return_value=[{"FileID": "1"}, {"FileID": "2"}]) comments_entries = [ { "Id": "18322", "Comment": "Hello there", "User": "Example User", "Created": "2023-03-23T07:45:29.056+0200", "Updated": "2023-03-23T07:45:29.056+0200", "UpdateUser": "Example User", } ] mocker.patch("JiraV3.get_comments_entries_for_fetched_incident", return_value=comments_entries) incidents = fetch_incidents( client=client, issue_field_to_fetch_from="updated date", fetch_query="status!=done", id_offset=1234, fetch_attachments=True, fetch_comments=True, max_fetch_incidents=3, first_fetch_interval="3 days", mirror_direction="Incoming And Outgoing", comment_tag_to_jira="comment_tag_to_jira", comment_tag_from_jira="comment_tag_from_jira", attachment_tag_to_jira="attachment_tag_to_jira", attachment_tag_from_jira="attachment_tag_from_jira", ) assert json.dumps(issue_incident) == incidents[0].get("rawJSON") def test_retrieve_smallest_issue_id_when_fetching_by_id_and_offset_is_zero(self, mocker): """ Given: - Arguments to use when calling the fetch incidents mechanism When - We are fetching by the issue ID, and the ID offset is set to 0 Then - Validate that the correct query is being called in order to retrieve the smallest issue ID with respect to the fetch query """ from JiraV3 import DEFAULT_FETCH_LIMIT, fetch_incidents client = jira_base_client_mock() mocker.patch("JiraV3.create_incident_from_issue", return_value={}) smallest_issue_id = "10161" run_query_mocker = mocker.patch.object(client, "run_query", side_effect=[{"issues": [{"id": smallest_issue_id}]}, {}]) fetch_query = "status!=done" fetch_incidents( client=client, issue_field_to_fetch_from="id", fetch_query=fetch_query, id_offset=0, fetch_attachments=True, fetch_comments=True, max_fetch_incidents=DEFAULT_FETCH_LIMIT, first_fetch_interval="3 days", mirror_direction="Incoming And Outgoing", comment_tag_to_jira="comment_tag_to_jira", comment_tag_from_jira="comment_tag_from_jira", attachment_tag_to_jira="attachment_tag_to_jira", attachment_tag_from_jira="attachment_tag_from_jira", ) assert ( run_query_mocker.call_args_list[0][1].get("query_params", {}).get("jql", "") == f"{fetch_query} ORDER BY created ASC" ) assert ( run_query_mocker.call_args_list[1][1].get("query_params", {}).get("jql", "") == f"{fetch_query} AND id >= {smallest_issue_id} ORDER BY id ASC" ) def test_fetch_incidents_by_id_incorrect_id_offset_error(self, mocker): """ Given: - Arguments to use when calling the fetch incidents mechanism When - We are fetching by the issue ID, and the ID offset is set to an arbitrary number, other than 0 Then - Validate that the error is caught, that stems from configuring an incorrect (does not exist) ID offset """ from JiraV3 import DEFAULT_FETCH_LIMIT, fetch_incidents client = jira_base_client_mock() mocker.patch("JiraV3.create_incident_from_issue", return_value={}) smallest_issue_id = "10161" mocker.patch.object( client, "run_query", side_effect=[ Exception("Issue does not exist or you do not have permission to see it"), {"issues": [{"id": smallest_issue_id}]}, ], ) fetch_query = "status!=done" with pytest.raises(DemistoException) as e: fetch_incidents( client=client, issue_field_to_fetch_from="id", fetch_query=fetch_query, id_offset=1, fetch_attachments=True, fetch_comments=True, max_fetch_incidents=DEFAULT_FETCH_LIMIT, first_fetch_interval="3 days", mirror_direction="Incoming And Outgoing", comment_tag_to_jira="comment_tag_to_jira", comment_tag_from_jira="comment_tag_from_jira", attachment_tag_to_jira="attachment_tag_to_jira", attachment_tag_from_jira="attachment_tag_from_jira", ) assert f"The smallest issue ID with respect to the fetch query is {smallest_issue_id}" in str(e) def test_fetch_incidents_by_id_and_offset_is_zero_error(self, mocker): """ Given: - Arguments to use when calling the fetch incidents mechanism When - We are fetching by the issue ID, the ID offset is set to 0, we try to acquire the smallest issue ID with respect to the fetch query, but there are no issues returned from the fetch query Then - Validate that an error is returned, stating that there are no issues with respect to the configured fetch query """ from JiraV3 import DEFAULT_FETCH_LIMIT, fetch_incidents client = jira_base_client_mock() mocker.patch("JiraV3.create_incident_from_issue", return_value={}) mocker.patch.object(client, "run_query", return_value={"issues": []}) fetch_query = "status!=done" with pytest.raises(DemistoException) as e: fetch_incidents( client=client, issue_field_to_fetch_from="id", fetch_query=fetch_query, id_offset=0, fetch_attachments=True, fetch_comments=True, max_fetch_incidents=DEFAULT_FETCH_LIMIT, first_fetch_interval="3 days", mirror_direction="Incoming And Outgoing", comment_tag_to_jira="comment_tag_to_jira", comment_tag_from_jira="comment_tag_from_jira", attachment_tag_to_jira="attachment_tag_to_jira", attachment_tag_from_jira="attachment_tag_from_jira", ) assert "The fetch query configured returned no Jira issues, please update it" in str(e) class TestJiraIssueAssign: @pytest.mark.parametrize( "assignee, assignee_id, excpected_body_request", [("server_assignee", None, {"name": "server_assignee"}), (None, "cloud_assignee", {"accountId": "cloud_assignee"})], ) def test_update_issue_assignee_command(self, mocker, assignee, assignee_id, excpected_body_request): """ Given: - issue id, and assignees for cloud/server jira When - Running the update_issue_assignee_command Then - Ensure the body request is ok for both cloud/server jira """ from JiraV3 import update_issue_assignee_command get_issue_response = util_load_json("test_data/get_issue_test/raw_response.json") args = { "assignee": assignee, # For Jira OnPrem "assignee_id": assignee_id, # For Jira Cloud "issue_id": 21487, } client: JiraBaseClient = jira_base_client_mock() if assignee_id: client = jira_cloud_client_mock() else: client = jira_onprem_client_mock() jira_req_mocker = mocker.patch.object(client, "update_assignee", return_value=None) mocker.patch.object(client, "get_issue", return_value=get_issue_response) assert update_issue_assignee_command(client=client, args=args) assert jira_req_mocker.call_args[1].get("assignee_body") == excpected_body_request def test_test_update_issue_assignee_command_no_assignees(self): """ Given: - issue id, without assignee / assignee_id When - Running the update_issue_assignee_command Then - Ensure an exception is raised """ from JiraV3 import update_issue_assignee_command args = { "assignee": None, # For Jira OnPrem "assignee_id": None, # For Jira Cloud "issue_id": 21487, } client = jira_base_client_mock() with pytest.raises(DemistoException): update_issue_assignee_command(client=client, args=args) class TestJiraIssueGetForms: @pytest.mark.parametrize("issue_id", [("TES-2"), ("")]) def test_issue_get_forms_command(self, mocker, issue_id): """ Given: - issue_id When - Running the issue_get_forms_command Then - Ensure the body request is ok """ from JiraV3 import issue_get_forms_command args = {"issue_id": issue_id} client: JiraBaseClient = jira_base_client_mock() client = jira_onprem_client_mock() raw_response_path = "test_data/get_issue_forms_test/raw_response.json" parsed_result_path = "test_data/get_issue_forms_test/parsed_result.json" issue_get_forms_response = util_load_json(raw_response_path) expected_command_results_context = util_load_json(parsed_result_path) mock_request = mocker.patch.object(client, "issue_get_forms", return_value=issue_get_forms_response) if issue_id: command_results = issue_get_forms_command(client=client, args=args) for command_result in command_results: assert expected_command_results_context == command_result.to_context() mock_request.assert_called_with(issue_id=issue_id) else: with pytest.raises(ValueError): issue_get_forms_command(client=client, args=args) class TestJiraGetUserInfo: @pytest.mark.parametrize( "key, username, account_id, raw_response_path, parsed_result_path", [ ( "JIRAUSER10000", None, None, "test_data/get_user_info_test/onprem_raw_response.json", "test_data/get_user_info_test/onprem_parsed_result.json", ), ( None, "firstlast", None, "test_data/get_user_info_test/onprem_raw_response.json", "test_data/get_user_info_test/onprem_parsed_result.json", ), ( None, None, "user@example.com", "test_data/get_user_info_test/cloud_raw_response.json", "test_data/get_user_info_test/cloud_parsed_result.json", ), (None, None, None, None, None), ], ) def test_get_user_info_command(self, mocker, key, username, account_id, raw_response_path, parsed_result_path): """ Given: - key, username or account_id for cloud/server jira When - Running the get_user_info_command Then - Ensure the body request is ok for both cloud/server jira """ from JiraV3 import get_user_info_command args = { "key": key, # For Jira OnPrem "username": username, # For Jira OnPrem "account_id": account_id, # For Jira Cloud } client: JiraBaseClient = jira_base_client_mock() if account_id: client = jira_cloud_client_mock() identifier = f"accountId={account_id}" elif key or username: client = jira_onprem_client_mock() if key: identifier = f"key={key}" else: identifier = f"username={username}" else: identifier = "" if identifier: get_user_info_response = util_load_json(raw_response_path) expected_command_results_context = util_load_json(parsed_result_path) mock_request = mocker.patch.object(client, "get_user_info", return_value=get_user_info_response) command_results = get_user_info_command(client=client, args=args) assert expected_command_results_context == command_results.to_context() mock_request.assert_called_with(identifier) else: with pytest.raises(ValueError): get_user_info_command(client=client, args=args) class TestJiraCreateMetadataIssueTypes: @pytest.mark.parametrize("project_id_or_key", [("test_project_id"), ("")]) def test_get_create_metadata_issue_types(self, mocker, project_id_or_key): """ Given: - project_id_or_key When: - running get_create_metadata_issue_types_command Then: - ensure the body request is ok """ from JiraV3 import get_create_metadata_issue_types_command args = { "project_id_or_key": project_id_or_key, } client: JiraBaseClient = jira_base_client_mock() raw_response_path = "test_data/get_create_metadata_issue_types_test/raw_response.json" parsed_result_path = "test_data/get_create_metadata_issue_types_test/parsed_result.json" metadata_response = util_load_json(raw_response_path) expected_context = util_load_json(parsed_result_path) mock_request = mocker.patch.object(client, "get_create_metadata_issue_types", return_value=metadata_response) if project_id_or_key: command_results = get_create_metadata_issue_types_command(client=client, args=args) assert expected_context == command_results.to_context() mock_request.assert_called_with(project_id_or_key=project_id_or_key, start_at=0, max_results=50) else: with pytest.raises(ValueError): get_create_metadata_issue_types_command(client=client, args=args) class TestJiraCreateMetadataField: @pytest.mark.parametrize("project_id_or_key, issue_type_id", [("test_project_id", "100"), ("", "")]) def test_get_create_metadata_field(self, mocker, project_id_or_key, issue_type_id): """ Given: - project_id_or_key - issue_type_id When: - running get_create_metadata_field_command Then: - ensure the body request is ok """ from JiraV3 import get_create_metadata_field_command args = { "project_id_or_key": project_id_or_key, "issue_type_id": issue_type_id, } client: JiraBaseClient = jira_base_client_mock() raw_response_path = "test_data/get_create_metadata_field_test/raw_response.json" parsed_result_path = "test_data/get_create_metadata_field_test/parsed_result.json" metadata_response = util_load_json(raw_response_path) expected_context = util_load_json(parsed_result_path) mock_request = mocker.patch.object(client, "get_create_metadata_field", return_value=metadata_response) if project_id_or_key and issue_type_id: command_results = get_create_metadata_field_command(client=client, args=args) assert expected_context == command_results.to_context() mock_request.assert_called_with( project_id_or_key=project_id_or_key, issue_type_id=issue_type_id, start_at=0, max_results=50 ) else: with pytest.raises(ValueError): get_create_metadata_field_command(client=client, args=args) def test_get_remote_data_preview_command(): # Given: Prepare the mock objects and inputs mock_client = Mock() args = {"id": "JIRA-123"} # Mock Jira issue object returned by client mock_issue = util_load_json("test_data/get_remote_data_preview/raw_response.json") mock_client.get_issue.return_value = mock_issue # When: Execute the command under test result = get_remote_data_preview_command(mock_client, args) # Then: Validate the outputs assert result.outputs_prefix == "QuickActionPreview" assert result.outputs_key_field == "id" expected_preview = QuickActionPreview( id="21487", title="XSOAR description test", description="Testing summary XSOAR mirroring", status="Backlog", assignee="Example User(admin@test.com)", creation_date="2023-03-01T11:34:49.730+0200", severity="Low", ).to_context() assert result.outputs == expected_preview # Validate interactions mock_client.get_issue.assert_called_once_with(issue_id_or_key="JIRA-123") @pytest.mark.parametrize( "url, expected_is_cloud", [ ("https://yourcompany.atlassian.net", True), ("https://api.atlassian.com/ex/jira/", True), ("https://www.callback.com", False), ("https://test.atlassian.net.evil.com", False), ("https://dummy_url", False), ], ) def test_is_jira_cloud_url(url, expected_is_cloud): """ Given: - Various URL strings including Jira Cloud URLs, on-premises URLs, and invalid URLs When: - Calling is_jira_cloud_url function Then: - Validate that True is returned for Jira Cloud URLs and False for others """ from JiraV3 import is_jira_cloud_url result = is_jira_cloud_url(url) assert result == expected_is_cloud