diff --git a/Packs/Redmine/.pack-ignore b/Packs/Redmine/.pack-ignore new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Packs/Redmine/.secrets-ignore b/Packs/Redmine/.secrets-ignore new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Packs/Redmine/Integrations/Redmine/README.md b/Packs/Redmine/Integrations/Redmine/README.md new file mode 100644 index 000000000000..c23c795b50ef --- /dev/null +++ b/Packs/Redmine/Integrations/Redmine/README.md @@ -0,0 +1,735 @@ +A project management and issue tracking system that provides a web-based platform for managing projects, tracking tasks, and handling various types of project-related activities. +This integration was integrated and tested with version 5.1.2 of Redmine. + +## Configure Redmine on Cortex XSOAR + +1. Navigate to **Settings** > **Integrations** > **Servers & Services**. +2. Search for Redmine. +3. Click **Add instance** to create and configure a new integration instance. + + | **Parameter** | **Required** | + | --- | --- | + | Server URL (e.g., ) | True | + | Trust any certificate (not secure) | False | + | API Key | True | + | Project id | False | +4. Getting your API key: + - Use your **server URL** to enter your Redmine instance. + - Authenticate with your username and password. + - Navigate to **My Account** (at the top right corner). + - Click API **Access key** > **Show** - This is your API key +5. Click **Test** to validate the URLs, token, and connection. + +## Commands + +You can execute these commands from the Cortex XSOAR CLI, as part of an automation, or in a playbook. +After you successfully execute a command, a DBot message appears in the War Room with the command details. + +### redmine-issue-create + +*** +- Create a new issue +- When attaching a file to an issue, include the entry ID in the request as file_entry_id=the ID you created +- To create a custom field, navigate to the server URL with administrative privileges, click **Administration** (located at the top left), select **Custom fields**, and then proceed to create a new custom field. Once created, you can add values as needed +- To create a category/version, navigate to the server URL > click **Settings** (top bar) > **Versions** tab and **Issue categories** tab. + + +#### Base Command + +`redmine-issue-create` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| project_id | The project ID for this issue. If not specified, the value from integration configuration will be taken. | Optional | +| tracker_id | The tracker ID for this issue. Possible values are: Bug, Feature, Support. | Required | +| status_id | The status ID for this issue. Possible values are: New, In progress, Resolved, Feedback, Closed, Rejected. | Required | +| priority_id | The priority ID for this issue. Possible values are: Low, Normal, High, Urgent, Immediate. | Required | +| subject | The subject for this issue. | Required | +| description | A description for this issue. | Optional | +| category_id | The category ID for this issue. | Optional | +| fixed_version_id | The target version ID for this issue. | Optional | +| assigned_to_id | The ID of the user to assign the issue to. | Optional | +| parent_issue_id | The ID of the parent issue. | Optional | +| custom_fields | The custom field to update. The format is costumFieldID:Value,costumFieldID:Value, etc. | Optional | +| watcher_user_ids | An array with watcher user IDs for this issue -> 1,2,3. | Optional | +| is_private | Is the issue private?. Possible values are: True, False. | Optional | +| estimated_hours | The number of hours estimated for this issue. | Optional | +| file_entry_id | The entry ID of the file to upload. | Optional | +| file_name | The name of the file to attach. Make sure the file name ends with .jpg/png/txt. | Optional | +| file_description | The description of the file you attached. | Optional | +| file_content_type | The file content type of the file you attached. | Optional | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| Redmine.Issue.id | srt | The ID of the new issue. | +| Redmine.Issue.priority.id | str | The ID of the priority of the issue. | +| Redmine.Issue.tracker.id | str | The ID of the tracker of the issue. | +| Redmine.Issue.project.id | str | The ID of the project of the issue. | +| Redmine.Issue.status.id | str | The ID of the status of the issue. | +| Redmine.Issue.subject | str | The subject of the issue. | + +#### Command example +```!redmine-issue-create priority_id=High status_id=Closed subject=helloExample tracker_id=Bug project_id=1 watcher_user_ids=5,6 custom_fields=1:helloCustom``` +#### Context Example +```json +{ + "Redmine": { + "Issue": { + "author": { + "id": 6, + "name": "Integration Test" + }, + "closed_on": null, + "created_on": "2024-03-11T09:16:47Z", + "custom_fields": [ + { + "id": 1, + "name": "Team_of_workers", + "value": "helloCustom" + } + ], + "description": null, + "done_ratio": 0, + "due_date": null, + "estimated_hours": null, + "id": "130", + "is_private": false, + "priority": { + "id": 3, + "name": "High" + }, + "project": { + "id": 1, + "name": "Cortex XSOAR" + }, + "start_date": "2024-03-11", + "status": { + "id": 1, + "is_closed": false, + "name": "New" + }, + "subject": "helloExample", + "total_estimated_hours": null, + "tracker": { + "id": 1, + "name": "Bug" + }, + "updated_on": "2024-03-11T09:16:47Z" + } + } +} +``` + +#### Human Readable Output + +>### The issue you created: +>|ID|Project|Tracker|Status| Priority|Author|Created On|Subject|Start Date|Custom Fields| +>|---|---|---|---|---|---|---|---|---|---| +>| 130 | Cortex XSOAR | Bug | New | High | Integration Test | 2024-03-11T09:16:47Z | helloExample | 2024-03-11 | **-** ***name***: Team_of_workers
***value***: helloCustom | + + +### redmine-issue-list + +*** +Display a list of issues + +#### Base Command + +`redmine-issue-list` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| page_number | The page number. | Optional | +| page_size | The page size. Default value is 50, if limit is not specified. | Optional | +| limit | The number of issues to display in the response (maximum is 100). If page_number or page_size are specified, this field will be ignored. Default is 25. | Optional | +| sort | A field by which to sort the results. Append ":desc" to invert the order.
- Possible values:
1. tracker.
2. status.
3. priority.
4. project.
5. subproject.
6. assigned_to.
- For example: sort=tracker:desc.
| Optional | +| include | An array of extra fields to fetch.
- Possible values:
1. attachments.
2. relations.
| Optional | +| issue_id | An array of issue IDs to display -> 1,2,3. | Optional | +| project_id | Aa project ID to display issues of this project. If not specified here or in the integration configuration, all projects will be displayed. | Optional | +| subproject_id | A subproject ID to display issues of this subproject (use "project_id=someID" and "subproject_id=!name_of_subproject" to exclude subprojects). | Optional | +| tracker_id | A tracker ID to display issues of this tracker ID. Possible values are: Bug, Feature, Support. | Optional | +| status_id | A status ID to display issues of this status ID (* means all). Possible values are: open, closed, *. | Optional | +| assigned_to_id | An assigned-to ID to display issues assigned to this user ID. | Optional | +| parent_id | A parent ID to display issues that are under this parent ID. | Optional | +| custom_field | - The custom field to filter by. The format is costumFieldID:Value.
- To filter according to the desired custom field, ensure that it is marked as 'used as a filter' and 'searchable' in your Redmine server settings.
- You can only filter one custom field at a time.
- Make sure the custom field ID you entered is valid, or the request won't fail but will not be filtered correctly.
| Optional | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| Redmine.Issue | dict | A list of issues. | +| Redmine.Issue.id | str | A list of issues. | + +#### Command example +```!redmine-issue-list limit=2``` +#### Context Example +```json +{ + "Redmine": { + "Issue": [ + { + "author": { + "id": 6, + "name": "Integration Test" + }, + "closed_on": null, + "created_on": "2024-03-11T09:16:47Z", + "custom_fields": [ + { + "id": 1, + "name": "Team_of_workers", + "value": "helloCustom" + } + ], + "description": null, + "done_ratio": 0, + "due_date": null, + "estimated_hours": null, + "id": "130", + "is_private": false, + "priority": { + "id": 3, + "name": "High" + }, + "project": { + "id": 1, + "name": "Cortex XSOAR" + }, + "spent_hours": 0, + "start_date": "2024-03-11", + "status": { + "id": 1, + "is_closed": false, + "name": "New" + }, + "subject": "subjectChanged", + "total_estimated_hours": null, + "total_spent_hours": 0, + "tracker": { + "id": 1, + "name": "Bug" + }, + "updated_on": "2024-03-11T09:16:54Z" + }, + { + "author": { + "id": 6, + "name": "Integration Test" + }, + "closed_on": null, + "created_on": "2024-03-11T09:08:09Z", + "custom_fields": [ + { + "id": 1, + "name": "Team_of_workers", + "value": "helloCustom" + } + ], + "description": null, + "done_ratio": 0, + "due_date": null, + "estimated_hours": null, + "id": "129", + "is_private": false, + "priority": { + "id": 3, + "name": "High" + }, + "project": { + "id": 1, + "name": "Cortex XSOAR" + }, + "spent_hours": 0, + "start_date": "2024-03-11", + "status": { + "id": 1, + "is_closed": false, + "name": "New" + }, + "subject": "helloExample", + "total_estimated_hours": null, + "total_spent_hours": 0, + "tracker": { + "id": 1, + "name": "Bug" + }, + "updated_on": "2024-03-11T09:08:09Z" + } + ] + } +} +``` + +#### Human Readable Output + +>#### Showing 2 results from page 1: +>### Issues Results: +>|ID|Tracker|Status| Priority|Author|Subject|Start Date|done_ratio|Is Private|Custom Fields|Created On|updated_on| +>|---|---|---|---|---|---|---|---|---|---|---|---| +>| 130 | Bug | New | High | Integration Test | subjectChanged | 2024-03-11 | 0 | false | **-** ***name***: Team_of_workers
***value***: helloCustom | 2024-03-11T09:16:47Z | 2024-03-11T09:16:54Z | +>| 129 | Bug | New | High | Integration Test | helloExample | 2024-03-11 | 0 | false | **-** ***name***: Team_of_workers
***value***: helloCustom | 2024-03-11T09:08:09Z | 2024-03-11T09:08:09Z | + + +### redmine-issue-update + +*** +Update an existing issue. When attaching a file to an issue, include the entry ID in the request as file_entry_id=the ID you created. To create a custom field, navigate to the server URL with administrative privileges, click **'Administration** (located at the top left), select **Custom fields**, and proceed to create a new custom field. Once created, you can add values as needed. + +#### Base Command + +`redmine-issue-update` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| issue_id | The ID of the issue to be updated. | Required | +| project_id | The ID of the project to associate with the issue. If not specified, the value from integration configuration will be taken if specified. | Optional | +| tracker_id | The ID of the tracker type. Possible values are: Bug, Feature, Support. | Optional | +| status_id | The ID of the status to set for the issue. Possible values are: New, In progress, Resolved, Feedback, Closed, Rejected. | Optional | +| priority_id | The ID of the priority level for the issue. Possible values are: Low, Normal, High, Urgent, Immediate. | Optional | +| subject | The subject of the issue. | Optional | +| description | The description of the issue. | Optional | +| category_id | The ID of the category to assign to the issue. | Optional | +| fixed_version_id | The ID of the fixed version for the issue. | Optional | +| assigned_to_id | The ID of the user to whom the issue is assigned. | Optional | +| parent_issue_id | The ID of the parent issue, if applicable. | Optional | +| custom_fields | The custom field to update. The format is costumFieldID:Value,costumFieldID:Value etc. | Optional | +| watcher_user_ids | A comma-separated list of watcher IDs. -> 1,2,3. | Optional | +| is_private | Is the issue private?. Possible values are: True, False. | Optional | +| estimated_hours | The estimated number of hours to complete the issue. | Optional | +| notes | Additional comments about the update. | Optional | +| private_notes | Specifies if the notes are private. Possible values are: True, False. | Optional | +| file_entry_id | The entry ID of the file to upload. Required if uploading a file. | Optional | +| file_name | The name of the file to upload (should end with .jpg/.png/.txt, etc.). | Optional | +| file_description | The description of the attached file. | Optional | +| file_content_type | The content type of the attached file (image/jpg or image/png or text/txt, etc.). | Optional | + +#### Context Output + +There is no context output for this command. +#### Command example +```!redmine-issue-update issue_id=130 subject=subjectChanged``` +#### Human Readable Output + +>Issue with id 130 was successfully updated. + +### redmine-issue-get + +*** +Show an issue by id + +#### Base Command + +`redmine-issue-get` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| issue_id | The ID of the issue you want to display. | Required | +| include | - Fields to add to the response.
- Possible values:
1.children.
2.attachments.
3.relations.
4.changesets.
5.journals.
6.watchers.
7.allowed_statuses.
- Separate multiple values with comma ONLY.
| Optional | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| Redmine.Issue.id | unknown | The ID of the found issue. | +| Redmine.Issue.priority.id | unknown | The ID of the priority of the issue. | +| Redmine.Issue.tracker.id | unknown | The ID of the tracker of the issue. | +| Redmine.Issue.project.id | unknown | The ID of the project of the issue. | +| Redmine.Issue.status.id | unknown | The ID of the status of the issue. | +| Redmine.Issue.subject | unknown | The subject of the issue. | +| Redmine.Issue.watchers.id | unknown | The watchers of the issue. | + +#### Command example +```!redmine-issue-get issue_id=130 include=watchers``` +#### Context Example +```json +{ + "Redmine": { + "Issue": { + "author": { + "id": 6, + "name": "Integration Test" + }, + "closed_on": null, + "created_on": "2024-03-11T09:16:47Z", + "custom_fields": [ + { + "id": 1, + "name": "Team_of_workers", + "value": "helloCustom" + } + ], + "description": null, + "done_ratio": 0, + "due_date": null, + "estimated_hours": null, + "id": "130", + "is_private": false, + "priority": { + "id": 3, + "name": "High" + }, + "project": { + "id": 1, + "name": "Cortex XSOAR" + }, + "spent_hours": 0, + "start_date": "2024-03-11", + "status": { + "id": 1, + "is_closed": false, + "name": "New" + }, + "subject": "subjectChanged", + "total_estimated_hours": null, + "total_spent_hours": 0, + "tracker": { + "id": 1, + "name": "Bug" + }, + "updated_on": "2024-03-11T09:16:54Z", + "watchers": [ + { + "id": 5, + "name": "admin tests" + }, + { + "id": 6, + "name": "Integration Test" + } + ] + } + } +} +``` + +#### Human Readable Output + +>### Issues List: +>|Id|Project|Tracker|Status|Priority|Author|Subject|StartDate|DoneRatio|IsPrivate|CustomFields|CreatedOn|Watchers| +>|---|---|---|---|---|---|---|---|---|---|---|---|---| +>| 130 | Cortex XSOAR | Bug | New | High | Integration Test | subjectChanged | 2024-03-11 | 0 | false | **-** ***name***: Team_of_workers
***value***: helloCustom | 2024-03-11T09:16:47Z | **-** ***name***: admin tests
**-** ***name***: Integration Test | + + +### redmine-issue-delete + +*** +Delete an issue by its ID + +#### Base Command + +`redmine-issue-delete` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| issue_id | The ID of the issue you want to delete. | Required | + +#### Context Output + +There is no context output for this command. +#### Command example +```!redmine-issue-delete issue_id=130``` +#### Human Readable Output + +>Issue with id 130 was deleted successfully. + +### redmine-issue-watcher-add + +*** +Add a watcher to the specified issue + +#### Base Command + +`redmine-issue-watcher-add` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| issue_id | The ID of the issue to which you want to add a watcher. | Required | +| watcher_id | The ID of the watcher you want to add to the issue. | Required | + +#### Context Output + +There is no context output for this command. +#### Command example +```!redmine-issue-watcher-add issue_id=130 watcher_id=1``` +#### Human Readable Output + +>Watcher with id 1 was added successfully to issue with id 130. + +### redmine-issue-watcher-remove + +*** +Remove a watcher of an issue + +#### Base Command + +`redmine-issue-watcher-remove` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| issue_id | The ID of the issue from which you want to remove the watcher. | Required | +| watcher_id | The ID of the watcher you want to remove from the issue. | Required | + +#### Context Output + +There is no context output for this command. +#### Command example +```!redmine-issue-watcher-remove issue_id=130 watcher_id=1``` +#### Human Readable Output + +>Watcher with id 1 was removed successfully from issue with id 130. + +### redmine-project-list + +*** +Retrieve a list of all projects, including both public and private ones that the user has access to. + +#### Base Command + +`redmine-project-list` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| include | - Specify which additional fields to include in the response.
- Choose from the following options:
1. trackers.
2. issue_categories
3. enabled_modules
4. time_entry_activities
5. issue_custom_fields
- Separate multiple values with comma ONLY.
| Optional | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| Redmine.Project | unknown | Display a list of projects accessible to the user. | + +#### Command example +```!redmine-project-list ``` +#### Context Example +```json +{ + "Redmine": { + "Project": { + "created_on": "2024-02-29T10:34:23Z", + "custom_fields": [ + { + "id": 3, + "name": "second_custom_field", + "value": null + } + ], + "description": "", + "homepage": "", + "id": "1", + "identifier": "cortex-xsoar", + "inherit_members": false, + "is_public": "True", + "name": "Cortex XSOAR", + "status": "1", + "updated_on": "2024-02-29T10:34:23Z" + } + } +} +``` + +#### Human Readable Output + +>### Projects List: +>|Id|Name|Identifier|Status|IsPublic|CreatedOn|UpdatedOn| +>|---|---|---|---|---|---|---| +>| 1 | Cortex XSOAR | cortex-xsoar | 1 | True | 2024-02-29T10:34:23Z | 2024-02-29T10:34:23Z | + + +### redmine-custom-field-list + +*** +Retrieve a list of all custom fields. + +#### Base Command + +`redmine-custom-field-list` + +#### Input + +There are no input arguments for this command. + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| Redmine.CustomField | dict | Retrieve details of all custom fields. | +| Redmine.CustomField.id | str | Display ids of custom fields. | +| Redmine.CustomField.name | str | Display names of custom fields. | +| Redmine.CustomField.customized_type | str | Display customized_type of custom fields. | + +#### Command example +```!redmine-custom-field-list``` +#### Context Example +```json +{ + "Redmine": { + "CustomField": [ + { + "customized_type": "issue", + "default_value": "", + "description": "specify the team of workers under this issue", + "field_format": "string", + "id": "1", + "is_filter": "True", + "is_required": "False", + "max_length": null, + "min_length": null, + "multiple": false, + "name": "Team_of_workers", + "regexp": "", + "roles": [], + "searchable": true, + "trackers": [ + { + "id": 1, + "name": "Bug" + }, + { + "id": 2, + "name": "Feature" + }, + { + "id": 3, + "name": "Support" + } + ], + "visible": true + }, + { + "customized_type": "project", + "default_value": "", + "description": "", + "field_format": "string", + "id": "3", + "is_filter": "False", + "is_required": "False", + "max_length": null, + "min_length": null, + "multiple": false, + "name": "second_custom_field", + "regexp": "", + "searchable": false, + "visible": true + } + ] + } +} +``` + +#### Human Readable Output + +>### Custom Fields List: +>|Id|Name|CustomizedType|FieldFormat|IsRequired|IsFilter|Searchable|Trackers| +>|---|---|---|---|---|---|---|---| +>| 1 | Team_of_workers | issue | string | False | True | | **-** ***id***: 1
***name***: Bug
**-** ***id***: 2
***name***: Feature
**-** ***id***: 3
***name***: Support | +>| 3 | second_custom_field | project | string | False | False | | | + + +### redmine-user-id-list + +*** +- Retrieve a list of users with optional filtering options. +- This command requires admin privileges in your Redmine account. + + +#### Base Command + +`redmine-user-id-list` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| status | Specify the status of users to retrieve. Possible values are: Active, Registered, Locked. | Optional | +| name | Search for users matching a specific name (searches in first name, last name, and email). | Optional | +| group_id | Specify the group ID to filter users by. | Optional | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| Redmine.Users | dict |A list of users. | +| Redmine.Users.id | str | A list of users IDs. | +| Redmine.Users.login | str | A list of users login usernames. | +| Redmine.Users.admin | str | A list of users admins permission. | +| Redmine.Users.firstname | str | A list of users first name. | +| Redmine.Users.lastname | str | A list of users last name. | +| Redmine.Users.mail | str | A list of users mails. | + +#### Command example +```!redmine-user-id-list``` +#### Context Example +```json +{ + "Redmine": { + "Users": [ + { + "admin": "True", + "created_on": "2024-02-28T19:47:56Z", + "firstname": "admin", + "id": "5", + "last_login_on": "2024-02-29T10:25:08Z", + "lastname": "tests", + "login": "admin", + "mail": "admin@redmine-test.local", + "passwd_changed_on": "2024-02-28T19:49:17Z", + "twofa_scheme": null, + "updated_on": "2024-02-28T19:50:49Z" + }, + { + "admin": "True", + "created_on": "2024-02-29T10:27:31Z", + "firstname": "Integration", + "id": "6", + "last_login_on": "2024-02-29T10:55:25Z", + "lastname": "Test", + "login": "demiadmin", + "mail": "demiadmin@redmine-test.local", + "passwd_changed_on": "2024-02-29T10:27:31Z", + "twofa_scheme": null, + "updated_on": "2024-02-29T10:27:31Z" + }, + { + "admin": "True", + "created_on": "2024-02-28T18:34:10Z", + "firstname": "UserName", + "id": "1", + "last_login_on": "2024-02-29T09:50:10Z", + "lastname": "LastName", + "login": "user", + "mail": "user@example.com", + "passwd_changed_on": null, + "twofa_scheme": null, + "updated_on": "2024-02-28T18:34:10Z" + } + ] + } +} +``` + +#### Human Readable Output + +>### Users List: +>|ID|Login|Admin|First Name|Last Name|Email|Created On|Last Login On| +>|---|---|---|---|---|---|---|---| +>| 5 | admin | True | admin | tests | admin@redmine-test.local | 2024-02-28T19:47:56Z | 2024-02-29T10:25:08Z | +>| 6 | demiadmin | True | Integration | Test | demiadmin@redmine-test.local | 2024-02-29T10:27:31Z | 2024-02-29T10:55:25Z | +>| 1 | user | True | UserName | LastName | user@example.com | 2024-02-28T18:34:10Z | 2024-02-29T09:50:10Z | + diff --git a/Packs/Redmine/Integrations/Redmine/Redmine.py b/Packs/Redmine/Integrations/Redmine/Redmine.py new file mode 100644 index 000000000000..7921629c2364 --- /dev/null +++ b/Packs/Redmine/Integrations/Redmine/Redmine.py @@ -0,0 +1,642 @@ +import demistomock as demisto +from CommonServerPython import * + +from CommonServerUserPython import * + +from typing import Any + +''' CONSTANTS ''' +PAGE_SIZE_DEFAULT_INT = 50 +DEFAULT_LIMIT_NUMBER = 25 +BASE_DEFAULT_PAGE_NUMBER_INT = 1 +BASE_DEFAULT_OFFSET_NUMBER = 0 +MAX_LIMIT = 100 +MIN_LIMIT = 0 + +INVALID_ID_DEMISTO_ERROR = "Invalid ID for one or more fields that request IDs. Please make sure all IDs are correct." +RESPONSE_NOT_IN_FORMAT_ERROR = "The request succeeded, but a parse error occurred." + +HR_SHOW_ONLY_NAME = JsonTransformer(keys=['name'], func=lambda hdr: hdr.get('name', '')) + +USER_STATUS_DICT = { + 'Active': '1', + 'Registered': '2', + 'Locked': '3', +} + +ISSUE_TRACKER_DICT = { + 'Bug': '1', + 'Feature': '2', + 'Support': '3'} + +ISSUE_STATUS_DICT = { + 'New': '1', + 'In progress': '2', + 'Resolved': '3', + 'Feedback': '4', + 'Closed': '5', + 'Rejected': '6', +} + +ISSUE_STATUS_FOR_LIST_COMMAND = { + 'Open': 'open', + 'Closed': 'closed', + 'All': '*' +} + +ISSUE_PRIORITY_DICT = { + 'Low': '1', + 'Normal': '2', + 'High': '3', + 'Urgent': '4', + 'Immediate': '5' +} + + +''' CLIENT CLASS ''' + + +class Client(BaseClient): + def __init__(self, server_url, api_key, verify=True, proxy=False, headers=None, auth=None, project_id=None): + super().__init__(base_url=server_url, verify=verify, proxy=proxy, headers=headers, auth=auth) + + self._post_put_header = {'Content-Type': 'application/json', 'X-Redmine-API-Key': api_key} + self._upload_file_header = {'Content-Type': 'application/octet-stream', 'X-Redmine-API-Key': api_key} + self._get_header = {'X-Redmine-API-Key': api_key} + self._project_id = project_id + + def create_file_token_request(self, args, entry_id): + file_content = get_file_content(entry_id) + response = self._http_request('POST', '/uploads.json', params=args, headers=self._upload_file_header, + data=file_content) + return response + + def create_issue_request(self, args, watcher_user_ids, project_id): + args['project_id'] = project_id + args['watcher_user_ids'] = watcher_user_ids + remove_nulls_from_dictionary(args) + body_for_request = {'issue': args} + response = self._http_request('POST', '/issues.json', params={}, + json_data=body_for_request, headers=self._post_put_header) + return response + + def update_issue_request(self, args, project_id, watcher_user_ids): + issue_id = args.pop('issue_id') + args['project_id'] = project_id + args['watcher_user_ids'] = watcher_user_ids + remove_nulls_from_dictionary(args) + response = self._http_request('PUT', f'/issues/{issue_id}.json', json_data={"issue": args}, headers=self._post_put_header, + empty_valid_codes=[204], return_empty_response=True) + return response + + def get_issues_list_request(self, project_id, tracker_id, status_id, offset_to_dict, limit_to_dict, exclude_subproject, + args: dict[str, Any]): + if exclude_subproject and args.get('subproject_id', None): + raise DemistoException("Specify only one of the following, subproject_id or exclude.") + elif exclude_subproject: + args['subproject_id'] = f'!{exclude_subproject}' + params = assign_params(tracker_id=tracker_id, project_id=project_id, status_id=status_id, + offset=offset_to_dict, limit=limit_to_dict, **args) + response = self._http_request('GET', '/issues.json', params=params, headers=self._get_header) + return response + + def delete_issue_by_id_request(self, issue_id): + response = self._http_request('DELETE', f'/issues/{issue_id}.json', headers=self._post_put_header, + empty_valid_codes=[200, 204, 201], return_empty_response=True) + return response + + def get_issue_by_id_request(self, issue_id, included_fields): + response = self._http_request('GET', f'/issues/{issue_id}.json', params={"include": included_fields}, + headers=self._get_header) + return response + + def add_issue_watcher_request(self, issue_id, watcher_id): + args_to_add = {'user_id': watcher_id} + response = self._http_request('POST', f'/issues/{issue_id}/watchers.json', params=args_to_add, + headers=self._post_put_header, empty_valid_codes=[200, 204, 201], + return_empty_response=True) + return response + + def remove_issue_watcher_request(self, issue_id, watcher_id): + response = self._http_request('DELETE', f'/issues/{issue_id}/watchers/{watcher_id}.json', headers=self._post_put_header, + empty_valid_codes=[200, 204, 201], return_empty_response=True) + return response + + def get_project_list_request(self, args: dict[str, Any]): + response = self._http_request('GET', '/projects.json', params=args, headers=self._get_header) + return response + + def get_custom_fields_request(self): + response = self._http_request('GET', '/custom_fields.json', headers=self._get_header) + return response + + def get_users_request(self, args: dict[str, Any]): + response = self._http_request('GET', '/users.json', params=args, headers=self._get_header) + return response + + +''' HELPER FUNCTIONS ''' + + +def check_include_validity(included_args, include_options): + """Checks if all include string is valid- all arguments are from predefined options + + Args: + include_arg (str): The string argument. + include_options (str): The include options for the request. + + Raises: + Raises a demisto error if one or more from the include are not given in the predefined options. + """ + included_args = argToList(included_args) + invalid_values = set(included_args) - set(include_options) + if invalid_values: + raise DemistoException(f"The 'include' argument should only contain values from {include_options}, separated by commas. " + f"These values are not in options {invalid_values}.") + + +def create_paging_header(page_size: int, page_number: int): + return '#### Showing' + (f' {page_size}') + ' results' + (f' from page {page_number}') + ':\n' + + +def adjust_paging_to_request(page_number, page_size, limit): + if page_number or page_size: + page_size = arg_to_number(page_size) or PAGE_SIZE_DEFAULT_INT + page_number = arg_to_number(page_number) or BASE_DEFAULT_PAGE_NUMBER_INT + generated_offset = (page_number - 1) * page_size + return generated_offset, page_size, page_number + limit = arg_to_number(limit) or DEFAULT_LIMIT_NUMBER + if limit > MAX_LIMIT or limit <= MIN_LIMIT: + raise DemistoException(f"Maximum limit is 100 and Minimum limit is 0, you provided {limit}") + return BASE_DEFAULT_OFFSET_NUMBER, limit, BASE_DEFAULT_PAGE_NUMBER_INT + + +def convert_args_to_request_format(args: Dict[str, Any]): + if tracker_id := args.pop('tracker_id', None): + if tracker_id not in ISSUE_TRACKER_DICT: + raise DemistoException("Predefined value for tracker_id is not in format.") + args['tracker_id'] = ISSUE_TRACKER_DICT[tracker_id] + if status_id := args.pop('status_id', None): + if status_id not in ISSUE_STATUS_DICT: + raise DemistoException("Predefined value for status_id is not in format.") + args['status_id'] = ISSUE_STATUS_DICT[status_id] + if priority_id := args.pop('priority_id', None): + if priority_id not in ISSUE_PRIORITY_DICT: + raise DemistoException("Predefined value for priority_id is not in format.") + args['priority_id'] = ISSUE_PRIORITY_DICT[priority_id] + if custom_fields := args.pop('custom_fields', None): + custom_fields = argToList(custom_fields) + try: + args['custom_fields'] = [{'id': field.split(':')[0], 'value': field.split(':')[1]} for field in custom_fields] + except Exception as e: + if 'list index out of range' in e.args[0]: + raise DemistoException("Custom fields not in format, please follow the instructions") + raise + + +def get_file_content(entry_id: str) -> bytes: + """Returns the XSOAR file entry's content. + + Args: + entry_id (str): The entry id inside XSOAR. + + Returns: + Tuple[str, bytes]: A tuple, where the first value is the file name, and the second is the + content of the file in bytes. + """ + get_file_path_res = demisto.getFilePath(entry_id) + file_path = get_file_path_res.pop('path') + file_bytes: bytes = b'' + with open(file_path, 'rb') as f: + file_bytes = f.read() + return file_bytes + + +def handle_file_attachment(client: Client, args: Dict[str, Any], entry_id: str): + """If a file was provided create a token and add to args + + Args: + client (Client) + args (Dict[str,Any]): Raw arguments dict from user + + Raises: + DemistoException: response not in format or could not create a token + """ + try: + file_name = args.pop('file_name', '') + file_description = args.pop('file_description', '') + content_type = args.pop('file_content_type', '') + args_for_file = assign_params(file_name=file_name, content_type=content_type) + token_response = client.create_file_token_request(args_for_file, entry_id) + if 'upload' not in token_response or 'token' not in token_response['upload']: + raise DemistoException(f"Could not upload file with entry id {entry_id}, please try again.") + uploads = assign_params(token=token_response['upload'].get('token', ''), + content_type=content_type, + filename=file_name, + description=file_description) + args['uploads'] = [uploads] + except DemistoException as e: + if "Could not upload file with entry id" in e.message: + raise DemistoException(e.message) + raise DemistoException("Could not create a token for your file- please try again." + f"With error {e}.") + + +''' COMMAND FUNCTIONS ''' + + +def test_module(client: Client): + message: str = '' + try: + if (get_users_command(client, {})): + message = 'ok' + return return_results(message) + except DemistoException as e: + if '401' in str(e) or 'Unauthorized' in str(e): + message = f'Authorization Error: make sure API Key is correctly set. Error: {e}' + else: + raise e + return return_results(message) + + +def create_issue_command(client: Client, args: dict[str, Any]) -> CommandResults: + project_id = args.pop('project_id', client._project_id) + if not project_id: + raise DemistoException('project_id field is missing in order to create an issue') + # Checks if a file needs to be added + entry_id = args.pop('file_entry_id', None) + if entry_id: + handle_file_attachment(client, args, entry_id) + # Change predefined values to id + convert_args_to_request_format(args) + watcher_user_ids = argToList(args.pop('watcher_user_ids', None)) + try: + response = client.create_issue_request(args, watcher_user_ids, project_id) + except DemistoException as e: + if 'Error in API call [422]' in e.message or 'Error in API call [404]' in e.message: + raise DemistoException(INVALID_ID_DEMISTO_ERROR) + raise + if 'issue' not in response: + raise DemistoException(RESPONSE_NOT_IN_FORMAT_ERROR) + issue_response = response['issue'] + headers = ['id', 'project', 'tracker', 'status', 'priority', 'author', 'estimated_hours', 'created_on', + 'subject', 'description', 'start_date', 'estimated_hours', 'custom_fields'] + # Issue id is a number and tableToMarkdown can't transform it if is_auto_json_transform is True + issue_response['id'] = str(issue_response['id']) + command_results = CommandResults( + outputs_prefix='Redmine.Issue', + outputs_key_field='id', + outputs=issue_response, + raw_response=issue_response, + readable_output=tableToMarkdown('The issue you created:', issue_response, headers=headers, + removeNull=True, headerTransform=string_to_table_header, + json_transform_mapping={ + "tracker": HR_SHOW_ONLY_NAME, + "status": HR_SHOW_ONLY_NAME, + "priority": HR_SHOW_ONLY_NAME, + "author": HR_SHOW_ONLY_NAME, + "project": HR_SHOW_ONLY_NAME, + "custom_fields": JsonTransformer(keys=["name", "value"]), + }) + ) + + return command_results + + +def update_issue_command(client: Client, args: dict[str, Any]): + issue_id = args.get('issue_id') + # Checks if a file needs to be added + entry_id = args.pop('file_entry_id', None) + if entry_id: + handle_file_attachment(client, args, entry_id) + convert_args_to_request_format(args) + watcher_user_ids = args.pop('watcher_user_ids', None) + if watcher_user_ids: + watcher_user_ids = argToList(watcher_user_ids) + project_id = args.pop('project_id', client._project_id) + try: + client.update_issue_request(args, project_id, watcher_user_ids) + except DemistoException as e: + if 'Error in API call [422]' in e.message or 'Error in API call [404]' in e.message: + raise DemistoException(INVALID_ID_DEMISTO_ERROR) + raise + command_results = CommandResults( + readable_output=f'Issue with id {issue_id} was successfully updated.') + return command_results + + +def get_issues_list_command(client: Client, args: dict[str, Any]): + def check_args_validity_and_convert_to_id(status_id: str, tracker_id: str, custom_field: str): + if status_id: + if status_id in ISSUE_STATUS_FOR_LIST_COMMAND: + status_id = ISSUE_STATUS_FOR_LIST_COMMAND[status_id] + else: + raise DemistoException("Invalid status ID, please use only predefined values.") + if tracker_id: + if tracker_id in ISSUE_TRACKER_DICT: + tracker_id = ISSUE_TRACKER_DICT[tracker_id] + else: + raise DemistoException("Invalid tracker ID, please use only predefined values.") + if custom_field: + try: + cf_in_format = argToList(custom_field, ':') + args[f'cf_{cf_in_format[0]}'] = cf_in_format[1] + except Exception as e: + raise DemistoException(f"Invalid custom field format, please follow the command description. Error: {e}.") + return status_id, tracker_id + + page_number = args.pop('page_number', None) + page_size = args.pop('page_size', None) + limit = args.pop('limit', None) + offset_to_dict, limit_to_dict, page_number_for_header = adjust_paging_to_request(page_number, page_size, limit) + status_id = args.pop('status_id', 'Open') + tracker_id = args.pop('tracker_id', None) + custom_field = args.pop('custom_field', None) + status_id, tracker_id = check_args_validity_and_convert_to_id(status_id, tracker_id, custom_field) + project_id = args.pop('project_id', client._project_id) + exclude_sub_project = args.pop('exclude', None) + try: + response = client.get_issues_list_request(project_id, tracker_id, status_id, + offset_to_dict, limit_to_dict, exclude_sub_project, args) + except DemistoException as e: + if 'Error in API call [422]' in e.message or 'Error in API call [404]' in e.message: + raise DemistoException(INVALID_ID_DEMISTO_ERROR) + raise + + try: + issues_response = response['issues'] + except Exception: + raise DemistoException(RESPONSE_NOT_IN_FORMAT_ERROR) + page_header = create_paging_header(len(issues_response), page_number_for_header) + + # Issue id is a number and tableToMarkdown can't transform it if is_auto_json_transform is True + for issue in issues_response: + issue['id'] = str(issue['id']) + + headers = ['id', 'tracker', 'status', 'priority', 'author', 'subject', 'description', 'start_date', 'due_date', + 'done_ratio', 'is_private', 'estimated_hours', 'custom_fields', 'created_on', 'updated_on', + 'closed_on', 'attachments', 'relations'] + + command_results = CommandResults( + outputs_prefix='Redmine.Issue', + outputs_key_field='id', + outputs=issues_response, + raw_response=issues_response, + readable_output=page_header + tableToMarkdown('Issues Results:', + issues_response, + headers=headers, + removeNull=True, + headerTransform=string_to_table_header, + is_auto_json_transform=True, + json_transform_mapping={ + "tracker": HR_SHOW_ONLY_NAME, + "status": HR_SHOW_ONLY_NAME, + "priority": HR_SHOW_ONLY_NAME, + "author": HR_SHOW_ONLY_NAME, + "custom_fields": JsonTransformer(keys=["name", "value"]), + } + ) + ) + return command_results + + +def get_issue_by_id_command(client: Client, args: dict[str, Any]): + try: + issue_id = args.get('issue_id') + included_fields = args.get('include') + if included_fields: + check_include_validity(included_fields, ['children', 'attachments', 'relations', + 'changesets', 'journals', 'watchers', 'allowed_statuses']) + try: + response = client.get_issue_by_id_request(issue_id, included_fields) + except DemistoException as e: + if 'Error in API call [422]' in e.message or 'Error in API call [404]' in e.message: + raise DemistoException(INVALID_ID_DEMISTO_ERROR) + elif 'Error in API call [403]' in e.message: + raise DemistoException(f"{e.message} It can be due to Invalid ID for one or more fields that request IDs, " + "Please make sure all IDs are correct") + raise + if "issue" not in response: + raise DemistoException(RESPONSE_NOT_IN_FORMAT_ERROR) + response_issue = response['issue'] + headers = ['id', 'project', 'tracker', 'status', 'priority', 'author', 'subject', 'description', 'start_date', + 'due_date', 'done_ratio', 'is_private', 'estimated_hours', 'custom_fields', 'created_on', 'closed_on', + 'attachments', 'watchers', 'children', 'relations', 'changesets', 'journals', 'allowed_statuses'] + + # Issue id is a number and tableToMarkdown can't transform it if is_auto_json_transform is True + if 'id' in response_issue: + response_issue['id'] = str(response_issue['id']) + command_results = CommandResults(outputs_prefix='Redmine.Issue', + outputs_key_field='id', + outputs=response_issue, + raw_response=response_issue, + readable_output=tableToMarkdown('Issues List:', response_issue, + headers=headers, + removeNull=True, + headerTransform=underscoreToCamelCase, + is_auto_json_transform=True, + json_transform_mapping={ + "tracker": HR_SHOW_ONLY_NAME, + "project": HR_SHOW_ONLY_NAME, + "status": HR_SHOW_ONLY_NAME, + "priority": HR_SHOW_ONLY_NAME, + "author": HR_SHOW_ONLY_NAME, + "custom_fields": + JsonTransformer(keys=["name", "value"]), + "watchers": JsonTransformer(keys=["name"]), + "attachments": + JsonTransformer(keys=["filename", + "content_url", + "content_type", + "description"] + ), + })) + return command_results + except Exception as e: + if 'Error in API call [422]' in e.args[0] or 'Error in API call [404]' in e.args[0]: + raise DemistoException("Invalid ID for one or more fields that request IDs " + "Please make sure all IDs are correct") + raise + + +def delete_issue_by_id_command(client: Client, args: dict[str, Any]): + issue_id = args.get('issue_id') + try: + client.delete_issue_by_id_request(issue_id) + except DemistoException as e: + if 'Error in API call [422]' in e.message or 'Error in API call [404]' in e.message: + raise DemistoException(INVALID_ID_DEMISTO_ERROR) + raise + command_results = CommandResults( + readable_output=f'Issue with id {issue_id} was deleted successfully.') + return command_results + + +def add_issue_watcher_command(client: Client, args: dict[str, Any]): + issue_id = args.get('issue_id') + watcher_id = args.get('watcher_id') + try: + client.add_issue_watcher_request(issue_id, watcher_id) + except DemistoException as e: + if 'Error in API call [422]' in e.message or 'Error in API call [404]' in e.message: + raise DemistoException(INVALID_ID_DEMISTO_ERROR) + elif 'Error in API call [403]' in e.message: + raise DemistoException(f"{e.message} It can be due to Invalid ID for one or more fields that request IDs, " + "Please make sure all IDs are correct") + raise + command_results = CommandResults( + readable_output=f'Watcher with id {watcher_id} was added successfully to issue with id {issue_id}.') + return command_results + + +def remove_issue_watcher_command(client: Client, args: dict[str, Any]): + issue_id = args.get('issue_id') + watcher_id = args.get('watcher_id') + try: + client.remove_issue_watcher_request(issue_id, watcher_id) + except DemistoException as e: + if 'Error in API call [422]' in e.message or 'Error in API call [404]' in e.message: + raise DemistoException(INVALID_ID_DEMISTO_ERROR) + elif 'Error in API call [403]' in e.message: + raise DemistoException(f"{e.message} It can be due to Invalid ID for one or more fields that request IDs, " + "Please make sure all IDs are correct.") + raise + command_results = CommandResults( + readable_output=f'Watcher with id {watcher_id} was removed successfully from issue with id {issue_id}.') + return command_results + + +def get_project_list_command(client: Client, args: dict[str, Any]): + include_arg = args.get('include', None) + if include_arg: + check_include_validity(include_arg, + ['trackers', 'issue_categories', 'enabled_modules', 'time_entry_activities', 'issue_custom_fields'] + ) + response = client.get_project_list_request(args) + if 'projects' not in response: + raise DemistoException(RESPONSE_NOT_IN_FORMAT_ERROR) + projects_response = response['projects'] + + headers = ['id', 'name', 'identifier', 'description', 'status', 'is_public', 'time_entry_activities', 'created_on', + 'updated_on', 'default_value', 'visible', 'roles', 'issue_custom_fields', 'enabled_modules', + 'issue_categories', 'trackers'] + # Some project fields are numbers and tableToMarkdown can't transform it if is_auto_json_transform is true + for project in projects_response: + project['id'] = str(project['id']) + project['status'] = str(project['status']) + project['is_public'] = str(project['is_public']) + + command_results = CommandResults(outputs_prefix='Redmine.Project', + outputs_key_field='id', + outputs=projects_response, + raw_response=projects_response, + readable_output=tableToMarkdown('Projects List:', projects_response, + headers=headers, + removeNull=True, + headerTransform=underscoreToCamelCase, + is_auto_json_transform=True), + ) + return command_results + + +def get_custom_fields_command(client: Client, args): + response = client.get_custom_fields_request() + if 'custom_fields' not in response: + raise DemistoException(RESPONSE_NOT_IN_FORMAT_ERROR) + custom_fields_response = response['custom_fields'] + headers = ['id', 'name', 'customized_type', 'field_format', 'regexp', 'max_length', 'is_required', 'is_filter', + 'searchable', 'trackers', 'issue_categories', 'enabled_modules', 'time_entry_activities', + 'issue_custom_fields'] + # Some custom fields are numbers and tableToMarkdown can't transform it if is_auto_json_transform is True + for custom_field in custom_fields_response: + custom_field['id'] = str(custom_field['id']) + custom_field['is_required'] = str(custom_field['is_required']) + custom_field['is_filter'] = str(custom_field['is_filter']) + + command_results = CommandResults(outputs_prefix='Redmine.CustomField', + outputs_key_field='id', + outputs=custom_fields_response, + raw_response=custom_fields_response, + readable_output=tableToMarkdown('Custom Fields List:', custom_fields_response, + headers=headers, + removeNull=True, + headerTransform=underscoreToCamelCase, + is_auto_json_transform=True + ) + ) + return command_results + + +def get_users_command(client: Client, args: dict[str, Any]): + status_string = args.get('status') + if status_string: + try: + args['status'] = USER_STATUS_DICT[status_string] + except Exception: + raise DemistoException("Invalid status value- please use the predefined options only.") + response = client.get_users_request(args) + try: + users_response = response['users'] + except Exception: + raise DemistoException(RESPONSE_NOT_IN_FORMAT_ERROR) + headers = ['id', 'login', 'admin', 'firstname', 'lastname', 'mail', 'created_on', 'last_login_on'] + # Some issue fields are numbers and tableToMarkdown can't transform it. + for user in users_response: + user['id'] = str(user['id']) + user['admin'] = str(user['admin']) + command_results = CommandResults(outputs_prefix='Redmine.Users', + outputs_key_field='id', + outputs=users_response, + raw_response=users_response, + readable_output=tableToMarkdown('Users List:', users_response, headers=headers, + removeNull=True, headerTransform=string_to_table_header, + is_auto_json_transform=True)) + return command_results + + +def main() -> None: + params = demisto.params() + args = demisto.args() + base_url = params.get('url') + verify_certificate = not params.get('insecure', False) + proxy = params.get('proxy', False) + api_key = params.get('credentials', {}).get('password', '') + project_id = params.get('project_id', None) + + command = demisto.command() + demisto.debug(f'Command being called is {command}') + + try: + commands = {'redmine-issue-create': create_issue_command, + 'redmine-issue-update': update_issue_command, + 'redmine-issue-get': get_issue_by_id_command, + 'redmine-issue-delete': delete_issue_by_id_command, + 'redmine-issue-watcher-add': add_issue_watcher_command, + 'redmine-issue-watcher-remove': remove_issue_watcher_command, + 'redmine-issue-list': get_issues_list_command, + 'redmine-project-list': get_project_list_command, + 'redmine-custom-field-list': get_custom_fields_command, + 'redmine-user-id-list': get_users_command} + + client = Client( + server_url=base_url, + verify=verify_certificate, + proxy=proxy, + api_key=api_key, + project_id=project_id) + + if command == 'test-module': + return_results(test_module(client)) + elif command in commands: + return_results(commands[command](client, args)) + else: + raise NotImplementedError(f"command {command} is not implemented.") + + except Exception as e: + return_error(f'Failed to execute {command} command.\nError:\n{str(e)}') + + +''' ENTRY POINT ''' + +if __name__ in ('__main__', '__builtin__', 'builtins'): + main() diff --git a/Packs/Redmine/Integrations/Redmine/Redmine.yml b/Packs/Redmine/Integrations/Redmine/Redmine.yml new file mode 100644 index 000000000000..4250cbc91ffa --- /dev/null +++ b/Packs/Redmine/Integrations/Redmine/Redmine.yml @@ -0,0 +1,389 @@ +commonfields: + id: Redmine + version: -1 +name: Redmine +display: Redmine +category: Utilities +description: 'A project management and issue tracking system that provides a web-based platform for managing projects, tracking tasks, and handling various types of project-related activities. ' +configuration: +- section: Connect + display: Server URL (e.g., https://1.1.1.1) + name: url + type: 0 + required: true +- display: Trust any certificate (not secure) + name: insecure + type: 8 + required: false +- section: Connect + display: "" + displaypassword: API Key + name: credentials + type: 9 + required: true + hiddenusername: true +- section: Collect + display: Project Id + name: project_id + additionalinfo: This project ID will be overridden by command project ID if one is provided within a command. + type: 0 + required: false +script: + commands: + - name: redmine-issue-create + description: |- + - Create a new issue + - When attaching a file to an issue, include the entry ID in the request as file_entry_id=the ID you created + - - To create a custom field, navigate to the server URL with administrative privileges, click **Administration** (located at the top left), select **Custom fields**,' and then proceed to create a new custom field. Once created, you can add values as needed. + - To create a category/version, navigate to the server URL -> Click on the Settings (top bar) -> Versions tab and Issue categories tab. + arguments: + - name: project_id + description: The project ID for this issue. If not specified, the value from the integration configuration will be used. + required: false + - name: tracker_id + auto: PREDEFINED + description: The tracker ID for this issue. + predefined: + - Bug + - Feature + - Support + required: true + - name: status_id + auto: PREDEFINED + required: true + description: The status ID for this issue. + predefined: + - New + - In progress + - Resolved + - Feedback + - Closed + - Rejected + - name: priority_id + auto: PREDEFINED + required: true + description: The priority ID for this issue. + predefined: + - Low + - Normal + - High + - Urgent + - Immediate + - name: subject + required: true + description: The subject for this issue. + - name: description + description: A description for this issue. + - name: category_id + description: The category ID for this issue. + - name: fixed_version_id + description: The target version ID for this issue. + - name: assigned_to_id + description: The ID of the user to assign the issue to. + - name: parent_issue_id + description: The ID of the parent issue. + - name: custom_fields + description: The custom field to update. The format is costumFieldID:Value,costumFieldID:Value, etc. + isArray: true + - name: watcher_user_ids + description: An array with watcher user IDs for this issue -> 1,2,3. + isArray: true + - name: is_private + description: Is the issue private? + auto: PREDEFINED + predefined: + - 'True' + - 'False' + - name: estimated_hours + description: The number of hours estimated for this issue. + - name: file_entry_id + description: The entry ID of the file to upload. + - name: file_name + description: The name of the file to attach. Make sure the file name ends with .jpg/png/txt. + - name: file_description + description: The description of the file you attached. + - name: file_content_type + description: The file content type of the file you attached. + outputs: + - contextPath: Redmine.Issue.id + description: The ID of the new issue. + - contextPath: Redmine.Issue.priority.id + description: The ID of the priority of the issue. + - contextPath: Redmine.Issue.tracker.id + description: The ID of the tracker of the issue. + - contextPath: Redmine.Issue.project.id + description: The ID of the project of the issue. + - contextPath: Redmine.Issue.status.id + description: The ID of the status of the issue. + - contextPath: Redmine.Issue.subject + description: The subject of the issue. + - name: redmine-issue-list + description: Display a list of issues. + arguments: + - name: page_number + description: The page number. + - name: page_size + description: The page size. Default value is 50, if limit not specified. + - name: limit + description: The number of issues to display in the response (maximum is 100). If page_number or page_size are specified, this field will be ignored. + defaultValue: '25' + - name: sort + description: | + - A field by which to sort the results. Append ":desc" to invert the order. + - Possible values: + 1. tracker. + 2. status. + 3. priority. + 4. project. + 5. subproject. + 6. assigned_to. + - For example: sort=tracker:desc. + - name: include + description: | + - An array of extra fields to retrieve. + - Possible values: + 1. attachments. + 2. relations. + isArray: true + - name: issue_id + description: An array of issue IDs to display -> 1,2,3. + isArray: true + - name: project_id + description: A project ID to display issues of this project. If not specified here or in the integration configuration, all projects will be displayed. + - name: subproject_id + description: A subproject ID to display issues of this subproject (you can't use both subproject_id and exclude). + - name: exclude + description: A subproject ID to exclude from the list (you can't use both subproject_id and exclude), you need to add a project_id for this field to be included. + - name: tracker_id + auto: PREDEFINED + description: A tracker ID to display issues of this tracker ID. + predefined: + - Bug + - Feature + - Support + - name: status_id + auto: PREDEFINED + description: The status ID to display issues of this status ID. + defaultValue: Open + predefined: + - Open + - Closed + - All + - name: assigned_to_id + description: An assigned-to ID to display issues assigned to this user ID. + - name: parent_id + description: A parent ID to display issues that are under this parent ID. + - name: custom_field + description: |- + - The custom field to filter by. The format is costumFieldID:Value. + - To filter according to the desired custom field, ensure that it is marked as 'used as a filter' and 'searchable' in your Redmine server settings. + - You can only filter one custom field at a time. + - Make sure the custom field id you entered is valid, or the request won't fail but will not be filtered correctly. + outputs: + - contextPath: Redmine.Issue + description: A list of issues. + - contextPath: Redmine.Issue.id + description: A list of issues. + - name: redmine-issue-update + description: Update an existing issue. When attaching a file to an issue, include the entry ID in the request as file_entry_id=the ID you created. To create a custom field, navigate to the server URL with administrative privileges, click **'Administration** (located at the top left), select **Custom fields**, and proceed to create a new custom field. Once created, you can add values as needed. + arguments: + - name: issue_id + description: The ID of the issue to be updated. + required: true + - name: project_id + description: The ID of the project to associate with the issue. If not specified, the value from the integration configuration will be used if specified. + - name: tracker_id + auto: PREDEFINED + description: The ID of the tracker type. + predefined: + - Bug + - Feature + - Support + - name: status_id + auto: PREDEFINED + description: The ID of the status to set for the issue. + predefined: + - New + - In progress + - Resolved + - Feedback + - Closed + - Rejected + - name: priority_id + auto: PREDEFINED + description: The ID of the priority level for the issue. + predefined: + - Low + - Normal + - High + - Urgent + - Immediate + - name: subject + description: The subject of the issue. + - name: description + description: The description of the issue. + - name: category_id + description: The ID of the category to assign to the issue. + - name: fixed_version_id + description: The ID of the fixed version for the issue. + - name: assigned_to_id + description: The ID of the user to whom the issue is assigned. + - name: parent_issue_id + description: The ID of the parent issue, if applicable. + - name: custom_fields + description: The custom field to update. The format is costumFieldID:Value,costumFieldID:Value etc. + isArray: true + - name: watcher_user_ids + description: A comma-separated list of watcher IDs. -> 1,2,3. + isArray: true + - name: is_private + auto: PREDEFINED + description: Is the issue private? + predefined: + - 'True' + - 'False' + - name: estimated_hours + description: The estimated number of hours to complete the issue. + - name: notes + description: Additional comments about the update. + - name: private_notes + auto: PREDEFINED + description: Specifies if the notes are private. + predefined: + - 'True' + - 'False' + - name: file_entry_id + description: The entry ID of the file to upload. Required if uploading a file. + - name: file_name + description: The name of the file to upload (should end with .jpg/.png/.txt, etc.). + - name: file_description + description: The description of the attached file. + - name: file_content_type + description: The content type of the attached file (image/jpg or image/png or text/txt, etc.). + - name: redmine-issue-get + description: Show an issue by ID. + arguments: + - name: issue_id + description: The ID of the issue you want to display. + required: true + - name: include + description: | + - Fields to add to the response. + - Possible values: + 1.children. + 2.attachments. + 3.relations. + 4.changesets. + 5.journals. + 6.watchers. + 7.allowed_statuses. + - Separate multiple values with commas ONLY. + isArray: true + outputs: + - contextPath: Redmine.Issue.id + description: The ID of the issue. + - contextPath: Redmine.Issue.priority.id + description: The ID of the priority of the issue. + - contextPath: Redmine.Issue.tracker.id + description: The ID of the tracker of the issue. + - contextPath: Redmine.Issue.project.id + description: The ID of the project of the issue. + - contextPath: Redmine.Issue.status.id + description: The ID of the status of the issue. + - contextPath: Redmine.Issue.subject + description: The subject of the issue. + - contextPath: Redmine.Issue.watchers.id + description: The watchers of the issue. + - name: redmine-issue-delete + description: Delete an issue by its ID. + arguments: + - name: issue_id + description: The ID of the issue you want to delete. + required: true + - name: redmine-issue-watcher-add + description: Add a watcher to the specified issue. Ensure that the watcher_id is accurate, as the Redmine API does not raise an exception for an incorrect watcher_id. + arguments: + - name: issue_id + description: The ID of the issue to which you want to add a watcher. + required: true + - name: watcher_id + description: The ID of the watcher you want to add to the issue. + required: true + - name: redmine-issue-watcher-remove + description: Remove a watcher of an issue. + arguments: + - name: issue_id + description: The ID of the issue from which you want to remove the watcher. + required: true + - name: watcher_id + description: The ID of the watcher you want to remove from the issue. + required: true + - name: redmine-project-list + description: Retrieve a list of all projects, including both public and private ones that the user has access to. + arguments: + - name: include + description: | + - The additional fields to include in the response. + - Choose from the following options: + 1. trackers. + 2. issue_categories + 3. enabled_modules + 4. time_entry_activities + 5. issue_custom_fields + - Separate multiple values with commas ONLY. + isArray: true + outputs: + - contextPath: Redmine.Project + description: Display a list of projects accessible to the user. + - name: redmine-custom-field-list + description: Retrieve a list of all custom fields. + arguments: [] + outputs: + - contextPath: Redmine.CustomField + description: The details of all custom fields. + - contextPath: Redmine.CustomField.id + description: The IDs of custom fields. + - contextPath: Redmine.CustomField.name + description: The names of custom fields. + - contextPath: Redmine.CustomField.customized_type + description: The customized_type of custom fields. + - name: redmine-user-id-list + description: | + - Retrieve a list of users with optional filtering options. + - This command requires admin privileges in your Redmine account. + arguments: + - auto: PREDEFINED + name: status + description: The status of users to retrieve. + predefined: + - 'Active' + - 'Registered' + - 'Locked' + - name: name + description: Search for users matching a specific name (searches in first name, last name, and email). + - name: group_id + description: The group ID to filter users by. + outputs: + - contextPath: Redmine.Users + description: A list of users. + - contextPath: Redmine.Users.id + description: A list of users IDs. + - contextPath: Redmine.Users.login + description: A list of users login usernames. + - contextPath: Redmine.Users.admin + description: A list of users admins permission. + - contextPath: Redmine.Users.firstname + description: A list of users first name. + - contextPath: Redmine.Users.lastname + description: A list of users last name. + - contextPath: Redmine.Users.mail + description: A list of users mails. + dockerimage: demisto/python3:3.10.13.89873 + isfetch: false + runonce: false + script: '-' + subtype: python3 + type: python +fromversion: 6.10.0 +tests: +- Redmine-Test diff --git a/Packs/Redmine/Integrations/Redmine/Redmine_description.md b/Packs/Redmine/Integrations/Redmine/Redmine_description.md new file mode 100644 index 000000000000..4cdc9bb5c4d1 --- /dev/null +++ b/Packs/Redmine/Integrations/Redmine/Redmine_description.md @@ -0,0 +1,19 @@ +## Redmine Integration + +- Redmine is a flexible project management web application. Written using the Ruby on Rails framework, it is cross-platform and cross-database. +- Redmine is open source and released under the terms of the GNU General Public License v2 (GPL). + +## Authentication: + +1. Navigate to your server URL to access your account. +2. Enter your username and password. +3. Go to "My Account" located at the top right corner of the page. +4. Click **API access key** and select **Show** to reveal your API key. +5. Input the API key into the Redmine integration authentication window. + +## General notes + +- Your API key determines your role in Redmine. +- If you have insufficient permissions when using commands such as "update" or "create," certain fields may remain unchanged due to insufficient privileges. In such cases, while the command itself will not fail, those particular fields will not be updated. +- If you would like to display data related only to a project with ID x-. Fill it in the Redmine integration authentication window. + \ No newline at end of file diff --git a/Packs/Redmine/Integrations/Redmine/Redmine_image.png b/Packs/Redmine/Integrations/Redmine/Redmine_image.png new file mode 100644 index 000000000000..dde3c770ede9 Binary files /dev/null and b/Packs/Redmine/Integrations/Redmine/Redmine_image.png differ diff --git a/Packs/Redmine/Integrations/Redmine/Redmine_test.py b/Packs/Redmine/Integrations/Redmine/Redmine_test.py new file mode 100644 index 000000000000..0ab14ffe2b0d --- /dev/null +++ b/Packs/Redmine/Integrations/Redmine/Redmine_test.py @@ -0,0 +1,910 @@ +import pytest +from Redmine import Client + + +@pytest.fixture +def redmine_client(url: str = 'url', verify_certificate: bool = True, proxy: bool = False, auth=('username', 'password')): + return Client(url, verify_certificate, proxy, auth=auth) + + +''' COMMAND FUNCTIONS TESTS ''' + + +def test_create_issue_command(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed without list id + When: + - redmine-issue-create command is executed + Then: + - The http request is called with the right arguments + """ + from Redmine import create_issue_command + http_request = mocker.patch.object(redmine_client, '_http_request') + http_request.return_value = {"issue": {"id": "1"}} + args = {'project_id': '1', 'issue_id': '1', 'subject': 'changeFromCode', 'tracker_id': 'Bug', 'watcher_user_ids': '[1]'} + create_issue_command(redmine_client, args=args) + http_request.assert_called_with('POST', '/issues.json', params={}, + json_data={'issue': {'issue_id': '1', 'subject': 'changeFromCode', + 'tracker_id': '1', 'watcher_user_ids': [1], 'project_id': '1'}}, + headers={'Content-Type': 'application/json', 'X-Redmine-API-Key': True}) + + +def test_create_issue_command_response(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed without list id + When: + - redmine-issue-create command is executed + Then: + - The http request return with the expected response + """ + from Redmine import create_issue_command + args = { + 'project_id': '1', + 'issue_id': '1', + 'subject': 'testResponse', + 'tracker_id': 'Bug' + } + create_issue_request_mock = mocker.patch.object(redmine_client, 'create_issue_request') + create_issue_request_mock.return_value = {'issue': {'id': '789', 'project': {'name': 'testing', 'id': '1'}, + 'subject': 'testResponse', 'tracker': {'name': 'Bug', 'id': '1'} + } + } + result = create_issue_command(redmine_client, args) + assert result.readable_output == ("### The issue you created:\n|Id|Project|Tracker|Subject|\n|---|---|---|---|\n" + "| 789 | testing | Bug | testResponse |\n") + + +def test_create_issue_command_invalid_custom_fields(redmine_client): + """ + Given: + - All relevant arguments for the command that is executed and invalid custom field format + When: + - redmine-issue-create command is executed + Then: + - A DemistoException is raised + """ + from Redmine import create_issue_command + from CommonServerPython import DemistoException + args = {'project_id': '1', 'custom_fields': 'jnlnj', 'issue_id': '1', 'subject': 'testSub', 'tracker_id': 'Bug', + 'watcher_user_ids': '[1]', 'status_id': 'New', 'priority_id': 'High'} + with pytest.raises(DemistoException) as e: + create_issue_command(redmine_client, args) + assert e.value.message == "Custom fields not in format, please follow the instructions" + + +def test_create_issue_command_no_token_created_for_file(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-issue-create command is executed + Then: + - The http request is called with the right arguments + """ + from Redmine import create_issue_command + from CommonServerPython import DemistoException + create_file_token_request_mock = mocker.patch.object(redmine_client, 'create_file_token_request') + create_file_token_request_mock.return_value = {'token': 'token123'} + args = {'project_id': '1', 'status_id': 'New', 'file_entry_id': 'a.png', 'issue_id': '1', + 'subject': 'testSub', 'tracker_id': 'Bug', 'watcher_user_ids': '[1]'} + with pytest.raises(DemistoException) as e: + create_issue_command(redmine_client, args) + create_file_token_request_mock.assert_called_with({}, 'a.png') + assert e.value.message == "Could not upload file with entry id a.png, please try again." + + +def test_create_issue_command_with_file(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-issue-create command is executed + Then: + - The http request is called with the right arguments + """ + from Redmine import create_issue_command + http_request = mocker.patch.object(redmine_client, '_http_request') + http_request.return_value = {"issue": {"id": "1"}} + create_file_token_request_mock = mocker.patch.object(redmine_client, 'create_file_token_request') + create_file_token_request_mock.return_value = {'upload': {'token': 'token123'}} + args = {'project_id': '1', 'file_entry_id': 'a.png', 'issue_id': '1', 'subject': 'testSub', 'tracker_id': 'Bug', + 'watcher_user_ids': '[1]'} + create_issue_command(redmine_client, args=args) + create_file_token_request_mock.assert_called_with({}, 'a.png') + http_request.assert_called_with('POST', '/issues.json', params={}, json_data={'issue': {'issue_id': '1', 'subject': 'testSub', + 'uploads': [{'token': 'token123'}], + 'tracker_id': '1', + 'watcher_user_ids': [1], + 'project_id': '1'}}, + headers={'Content-Type': 'application/json', 'X-Redmine-API-Key': True}) + + +def test_create_issue_command_with_file_response(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-issue-create command is executed + Then: + - The http request return with the expected response with file + """ + from Redmine import create_issue_command + args = { + 'project_id': '1', + 'subject': 'testResponse', + 'tracker_id': 'Bug', + 'file_entry_id': '139i401hivnaflkm', + 'file_name': 'test file response' + } + create_file_token_request_mock = mocker.patch.object(redmine_client, 'create_file_token_request') + create_file_token_request_mock.return_value = {'upload': {'token': '111111'}} + create_issue_request_mock = mocker.patch.object(redmine_client, 'create_issue_request') + create_issue_request_mock.return_value = {'issue': {'id': '789', 'project': {'name': 'testing', 'id': '1'}, + 'subject': 'testResponse', 'tracker': {'name': 'Bug', 'id': '1'} + } + } + result = create_issue_command(redmine_client, args) + assert args.get('uploads', {})[0].get('token') == '111111' + assert args.get('uploads', {})[0].get('filename') == 'test file response' + assert result.readable_output == ("### The issue you created:\n|Id|Project|Tracker|Subject|\n" + "|---|---|---|---|\n| 789 | testing | Bug | testResponse |\n") + + +def test_create_issue_command_with_file_invalid_token_response(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-issue-create command is executed + Then: + - Raises an error on token response + """ + from Redmine import create_issue_command + from CommonServerPython import DemistoException + args = { + 'project_id': '1', + 'subject': 'testResponse', + 'tracker_id': 'Bug', + 'file_entry_id': '139i401hivnaflkm', + 'file_name': 'test file response' + } + create_file_token_request_mock = mocker.patch.object(redmine_client, 'create_file_token_request') + create_file_token_request_mock.return_value = {'upload': {'tokens': '111111'}} + with pytest.raises(DemistoException) as e: + create_issue_command(redmine_client, args) + assert e.value.message == "Could not upload file with entry id 139i401hivnaflkm, please try again." + + +def test_update_issue_command(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-issue-update command is executed + Then: + - The http request is called with the right arguments + """ + from Redmine import update_issue_command + http_request = mocker.patch.object(redmine_client, '_http_request') + args = {'issue_id': '1', 'subject': 'changeFromCode', 'tracker_id': 'Bug', 'watcher_user_ids': '[1]'} + update_issue_command(redmine_client, args=args) + http_request.assert_called_with('PUT', '/issues/1.json', json_data={'issue': {'subject': 'changeFromCode', + 'tracker_id': '1', 'watcher_user_ids': [1]}}, + headers={'Content-Type': 'application/json', 'X-Redmine-API-Key': True}, + empty_valid_codes=[204], return_empty_response=True) + + +def test_update_issue_command_response(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-issue-update command is executed + Then: + - The http request returns the right response + """ + from Redmine import update_issue_command + update_issue_request_mock = mocker.patch.object(redmine_client, 'update_issue_request') + args = {'issue_id': '1', 'subject': 'changefortest', 'tracker_id': 'Bug', 'watcher_user_ids': '[1]'} + update_issue_request_mock.return_value = {} + result = update_issue_command(redmine_client, args=args) + assert result.readable_output == 'Issue with id 1 was successfully updated.' + + +def test_update_issue_command_invalid_custom_fields(redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-issue-update command is executed + Then: + - Raises a custom fields not in format error + """ + from Redmine import update_issue_command + from CommonServerPython import DemistoException + args = {'custom_fields': 'jnlnj', 'issue_id': '1', 'subject': 'testSub', 'tracker_id': 'Bug', 'watcher_user_ids': '[1]', + 'status_id': 'New', 'priority_id': 'High'} + with pytest.raises(DemistoException) as e: + update_issue_command(redmine_client, args) + assert e.value.message == "Custom fields not in format, please follow the instructions" + + +def test_update_issue_command_no_token_created_for_file(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-issue-delete command is executed + Then: + - The http request is called with the right arguments + """ + from Redmine import update_issue_command + from CommonServerPython import DemistoException + create_file_token_request_mock = mocker.patch.object(redmine_client, 'create_file_token_request') + create_file_token_request_mock.return_value = {'token': 'token123'} + args = {'status_id': 'New', 'file_entry_id': 'a.png', 'issue_id': '1', + 'subject': 'testSub', 'tracker_id': 'Bug', 'watcher_user_ids': '[1]'} + with pytest.raises(DemistoException) as e: + update_issue_command(redmine_client, args) + create_file_token_request_mock.assert_called_with({}, 'a.png') + assert e.value.message == ("Could not upload file with entry id a.png, please try again.") + + +def test_update_issue_command_with_file(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed without list id + When: + - redmine-issue-update command is executed + Then: + - The http request is called with the right arguments + """ + from Redmine import update_issue_command + http_request = mocker.patch.object(redmine_client, '_http_request') + create_file_token_request_mock = mocker.patch.object(redmine_client, 'create_file_token_request') + create_file_token_request_mock.return_value = {'upload': {'token': 'token123'}} + args = {'file_entry_id': 'a.png', 'issue_id': '1', 'subject': 'testSub', 'tracker_id': 'Bug', 'watcher_user_ids': '[1]'} + update_issue_command(redmine_client, args=args) + create_file_token_request_mock.assert_called_with({}, 'a.png') + http_request.assert_called_with('PUT', '/issues/1.json', json_data={'issue': {'subject': 'testSub', 'tracker_id': '1', + 'uploads': [{'token': 'token123'}], + 'watcher_user_ids': [1]}}, headers={ + 'Content-Type': 'application/json', 'X-Redmine-API-Key': True}, empty_valid_codes=[204], + return_empty_response=True) + + +def test_get_issues_list_command(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed with asset id + When: + - redmine-issue-list command is executed + Then: + - The http request is called with the right arguments + """ + from Redmine import get_issues_list_command + http_request = mocker.patch.object(redmine_client, '_http_request') + args = {'sort': 'priority:desc', 'limit': '1'} + get_issues_list_command(redmine_client, args) + http_request.assert_called_with('GET', '/issues.json', params={'status_id': 'open', 'offset': 0, 'limit': 1, + 'sort': 'priority:desc'}, headers={'X-Redmine-API-Key': True}) + + +def test_get_issues_list_command_response(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed with asset id + When: + - redmine-issue-list command is executed + Then: + - The http request returns the right response + """ + from Redmine import get_issues_list_command + get_issues_list_request_mock = mocker.patch.object(redmine_client, 'get_issues_list_request') + get_issues_list_request_mock.return_value = {"issues": [{"id": "1", + "tracker": {"name": "Bug", "id": "1"}, + "status": {"name": "new", "id": "1"}, + "priority": {"name": "High", "id": "1"}, + "subject": "helloTest" + }] + } + args = {'sort': 'priority:desc', 'limit': '1'} + result = get_issues_list_command(redmine_client, args) + assert result.readable_output == ("#### Showing 1 results from page 1:\n### Issues Results:\n" + "|Id|Tracker|Status|Priority|Subject|\n|---|---|---|---|---|\n" + "| 1 | Bug | new | High | helloTest |\n") + + +def test_get_issues_list_command_invalid_response(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed with asset id + When: + - redmine-issue-list command is executed + Then: + - raises a response format error + """ + from Redmine import get_issues_list_command + from CommonServerPython import DemistoException + get_issues_list_request_mock = mocker.patch.object(redmine_client, 'get_issues_list_request') + get_issues_list_request_mock.return_value = {"issue": [{"id": "1", + "tracker": {"name": "Bug", "id": "1"}, + "status": {"name": "new", "id": "1"}, + "priority": {"name": "High", "id": "1"}, + "subject": "helloTest" + }] + } + args = {'sort': 'priority:desc', 'limit': '1'} + with pytest.raises(DemistoException) as e: + get_issues_list_command(redmine_client, args) + assert e.value.message == "The request succeeded, but a parse error occurred." + + +def test_get_issues_list_command_invalid_custom_field(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed with asset id + When: + - redmine-issue-list command is executed + Then: + - Raises an exception for invalid custom field + """ + from Redmine import get_issues_list_command + from CommonServerPython import DemistoException + with pytest.raises(DemistoException) as e: + get_issues_list_command(redmine_client, {'custom_field': 'frf2rg2'}) + assert e.value.message == ("Invalid custom field format, please follow the command description." + " Error: list index out of range.") + + +def test_get_issues_list_command_use_both_exclude_subproject(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed with asset id + When: + - redmine-issue-list command is executed + Then: + - Raises an exception for invalid usage of both exclude and subproject fields + """ + from Redmine import get_issues_list_command + from CommonServerPython import DemistoException + with pytest.raises(DemistoException) as e: + get_issues_list_command(redmine_client, {'subproject_id': '1', 'exclude': '2'}) + assert e.value.message == "Specify only one of the following, subproject_id or exclude." + + +def test_get_issues_list_command_invalid_status(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed with asset id + When: + - redmine-issue-list command is executed + Then: + - The http request is called with the right arguments + """ + from Redmine import get_issues_list_command + from CommonServerPython import DemistoException + with pytest.raises(DemistoException) as e: + get_issues_list_command(redmine_client, {'status_id': 'hhjuhkk'}) + assert e.value.message == "Invalid status ID, please use only predefined values." + + +def test_get_issue_by_id_command(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed without asset id + When: + - redmine-issue-show command is executed + Then: + - The http request is called with the right arguments + """ + from Redmine import get_issue_by_id_command + http_request = mocker.patch.object(redmine_client, '_http_request') + http_request.return_value = {"issue": {"id": "1"}} + args = {'issue_id': '1', 'include': 'watchers,attachments'} + get_issue_by_id_command(redmine_client, args) + http_request.assert_called_with('GET', '/issues/1.json', + params={'include': 'watchers,attachments'}, headers={'X-Redmine-API-Key': True}) + + +def test_get_issue_by_id_command_response(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed without asset id + When: + - redmine-issue-show command is executed + Then: + - The http request returns the right response + """ + from Redmine import get_issue_by_id_command + get_issue_by_id_request_mock = mocker.patch.object(redmine_client, 'get_issue_by_id_request') + get_issue_by_id_request_mock.return_value = {"issue": {"id": "1", + "tracker": {"name": "Bug", "id": "1"}, + "status": {"name": "new", "id": "1"}, + "priority": {"name": "High", "id": "1"}, + "subject": "helloTest", + "watchers": [{"name": "testingWatch", "id": "1"}] + } + } + args = {'issue_id': '1', 'include': 'watchers,attachments'} + result = get_issue_by_id_command(redmine_client, args) + assert result.readable_output == ("### Issues List:\n|Id|Tracker|Status|Priority|Subject|Watchers|\n|---|---|---|---|---|---|" + "\n| 1 | Bug | new | High | helloTest | **-** ***name***: testingWatch |\n") + + +def test_get_issue_by_id_command_invalid_response(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed without asset id + When: + - redmine-issue-show command is executed + Then: + - raises a response format error + """ + from Redmine import get_issue_by_id_command + from CommonServerPython import DemistoException + get_issue_by_id_request_mock = mocker.patch.object(redmine_client, 'get_issue_by_id_request') + get_issue_by_id_request_mock.return_value = {"id": "1", + "tracker": {"name": "Bug", "id": "1"}, + "status": {"name": "new", "id": "1"}, + "priority": {"name": "High", "id": "1"}, + "subject": "helloTest", + "watchers": {"name": "testingWatch", "id": "1"} + } + args = {'issue_id': '1', 'include': 'watchers,attachments'} + with pytest.raises(DemistoException) as e: + get_issue_by_id_command(redmine_client, args) + assert e.value.message == "The request succeeded, but a parse error occurred." + + +def test_get_issue_by_id_command_invalid_include_argument(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-issue-show command is executed + Then: + - No issue id raises a DemistoException + """ + from Redmine import get_issue_by_id_command + from CommonServerPython import DemistoException + args = {'sort': 'priority:desc', 'limit': '1', 'include': 'beikbfqi'} + with pytest.raises(DemistoException) as e: + get_issue_by_id_command(redmine_client, args) + assert e.value.message == ("The 'include' argument should only contain values from ['children', 'attachments', 'relations', " + "'changesets', 'journals', 'watchers', 'allowed_statuses'], separated by commas. " + "These values are not in options {'beikbfqi'}.") + + +def test_delete_issue_by_id_command_response(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-issue-delete command is executed + Then: + - The http request returns the right response + """ + from Redmine import delete_issue_by_id_command + delete_issue_by_id_request_mock = mocker.patch.object(redmine_client, 'delete_issue_by_id_request') + delete_issue_by_id_request_mock.return_value = {} + args = {'issue_id': '41'} + result = delete_issue_by_id_command(redmine_client, args) + assert result.readable_output == "Issue with id 41 was deleted successfully." + + +def test_delete_issue_by_id_command(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-issue-delete command is executed + Then: + - The http request is called with the right arguments + """ + from Redmine import delete_issue_by_id_command + http_request = mocker.patch.object(redmine_client, '_http_request') + args = {'issue_id': '41'} + delete_issue_by_id_command(redmine_client, args) + http_request.assert_called_with('DELETE', '/issues/41.json', headers={'Content-Type': 'application/json', + 'X-Redmine-API-Key': True}, empty_valid_codes=[200, 204, 201], return_empty_response=True) + + +def test_delete_issue_by_id_command_invalid_issue_id(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-issue-delete command is executed + Then: + - No issue id raises a DemistoException + """ + from Redmine import add_issue_watcher_command + from CommonServerPython import DemistoException + args = {'issue_id': '-1'} + http_request = mocker.patch.object(redmine_client, '_http_request') + http_request.side_effect = DemistoException( + "Invalid ID for one or more fields that request IDs. Please make sure all IDs are correct.") + with pytest.raises(DemistoException) as e: + add_issue_watcher_command(redmine_client, args) + assert e.value.message == "Invalid ID for one or more fields that request IDs. Please make sure all IDs are correct." + + +def test_add_issue_watcher_command_response(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-issue-watcher-add command is executed + Then: + - The http request returns the right response + """ + from Redmine import add_issue_watcher_command + add_issue_watcher_request_mock = mocker.patch.object(redmine_client, 'add_issue_watcher_request') + add_issue_watcher_request_mock.return_value = {} + args = {'issue_id': '1', 'watcher_id': '1'} + result = add_issue_watcher_command(redmine_client, args) + assert result.readable_output == "Watcher with id 1 was added successfully to issue with id 1." + + +def test_add_issue_watcher_command(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-issue-watcher-add command is executed + Then: + - The http request is called with the right arguments + """ + from Redmine import add_issue_watcher_command + http_request = mocker.patch.object(redmine_client, '_http_request') + args = {'issue_id': '1', 'watcher_id': '1'} + add_issue_watcher_command(redmine_client, args) + http_request.assert_called_with('POST', '/issues/1/watchers.json', params={'user_id': '1'}, + headers={'Content-Type': 'application/json', 'X-Redmine-API-Key': True}, + empty_valid_codes=[200, 204, 201], return_empty_response=True) + + +def test_add_issue_watcher_command_invalid_issue_watcher_id(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-issue-watcher-add command is executed + Then: + - No issue id raises a DemistoException + """ + from Redmine import add_issue_watcher_command + from CommonServerPython import DemistoException + args = {'issue_id': '-1', 'watcher_id': '-20'} + http_request = mocker.patch.object(redmine_client, '_http_request') + http_request.side_effect = DemistoException( + "Invalid ID for one or more fields that request IDs. Please make sure all IDs are correct.") + with pytest.raises(DemistoException) as e: + add_issue_watcher_command(redmine_client, args) + assert e.value.message == "Invalid ID for one or more fields that request IDs. Please make sure all IDs are correct." + + +def test_remove_issue_watcher_command(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-issue-watcher-remove command is executed + Then: + - The http request is called with the right arguments + """ + from Redmine import remove_issue_watcher_command + http_request = mocker.patch.object(redmine_client, '_http_request') + args = {'issue_id': '1', 'watcher_id': '1'} + remove_issue_watcher_command(redmine_client, args) + http_request.assert_called_with('DELETE', '/issues/1/watchers/1.json', headers={'Content-Type': 'application/json', + 'X-Redmine-API-Key': True}, + empty_valid_codes=[200, 204, 201], return_empty_response=True) + + +def test_remove_issue_watcher_command_response(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-issue-watcher-remove command is executed + Then: + - The http request returns the right response + """ + from Redmine import remove_issue_watcher_command + remove_issue_watcher_request_mock = mocker.patch.object(redmine_client, 'remove_issue_watcher_request') + remove_issue_watcher_request_mock.return_value = {} + args = {'issue_id': '1', 'watcher_id': '1'} + result = remove_issue_watcher_command(redmine_client, args) + assert result.readable_output == "Watcher with id 1 was removed successfully from issue with id 1." + + +def test_remove_issue_watcher_command_invalid_issue_watcher_id(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-issue-watcher-remove command is executed + Then: + - No issue id raises a DemistoException + """ + from Redmine import remove_issue_watcher_command + from CommonServerPython import DemistoException + args = {'issue_id': '-1', 'watcher_id': '-20'} + http_request = mocker.patch.object(redmine_client, '_http_request') + http_request.side_effect = DemistoException( + "Invalid ID for one or more fields that request IDs. Please make sure all IDs are correct.") + with pytest.raises(DemistoException) as e: + remove_issue_watcher_command(redmine_client, args) + assert e.value.message == "Invalid ID for one or more fields that request IDs. Please make sure all IDs are correct." + + +def test_get_project_list_command(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-project-list command is executed + Then: + - The http request is called with the right arguments + """ + from Redmine import get_project_list_command + http_request = mocker.patch.object(redmine_client, '_http_request') + http_request.return_value = {"projects": [{"id": "1", "status": "active", "is_public": "true"}]} + args = {'include': 'time_entry_activities'} + get_project_list_command(redmine_client, args) + http_request.assert_called_with('GET', '/projects.json', params={'include': 'time_entry_activities'}, + headers={'X-Redmine-API-Key': True}) + + +def test_get_project_list_command_response(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-project-list command is executed + Then: + - The http request returns the right response + """ + from Redmine import get_project_list_command + get_project_list_request_mock = mocker.patch.object(redmine_client, 'get_project_list_request') + get_project_list_request_mock.return_value = {"projects": [{"id": "1", "name": "testProject", "issue_custom_fields": + {"id": "1", "name": "custom"}, "status": "open", + "is_public": True}]} + args = {"include": "issue_custom_fields"} + result = get_project_list_command(redmine_client, args) + assert result.readable_output == ("### Projects List:\n|Id|Name|Status|IsPublic|IssueCustomFields|\n|---|---|---|---|---|" + "\n| 1 | testProject | open | True | ***id***: 1
***name***: custom |\n") + + +def test_get_project_list_command_invalid_include(redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-project-list command is executed + Then: + - No issue id raises a DemistoException + """ + from Redmine import get_project_list_command + from CommonServerPython import DemistoException + args = {'include': 'time_entry_activities,jissue_categories'} + with pytest.raises(DemistoException) as e: + get_project_list_command(redmine_client, args) + assert e.value.message == ("The 'include' argument should only contain values from ['trackers', 'issue_categories', " + "'enabled_modules', 'time_entry_activities', 'issue_custom_fields'], separated by commas." + " These values are not in options {'jissue_categories'}.") + + +def test_get_project_list_command_invalid_response(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-user-id-list command is executed + Then: + - The http request is called with the right arguments + """ + from Redmine import get_project_list_command + from CommonServerPython import DemistoException + args = {'status_id': 'open'} + mocker.patch.object(Client, 'get_project_list_request', return_value={'projectsss': {}}) + with pytest.raises(DemistoException) as e: + get_project_list_command(redmine_client, args) + assert e.value.message == "The request succeeded, but a parse error occurred." + + +def test_get_custom_fields_command(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-custom-field-list command is executed + Then: + - The http request is called with the right arguments + """ + from Redmine import get_custom_fields_command + http_request = mocker.patch.object(redmine_client, '_http_request') + http_request.return_value = {"custom_fields": [{"id": "1", "is_required": True, "is_filter": False}]} + get_custom_fields_command(redmine_client, {}) + http_request.assert_called_with('GET', '/custom_fields.json', headers={'X-Redmine-API-Key': True}) + + +def test_get_custom_fields_command_response(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-custom-field-list command is executed + Then: + - The http request returns the right response + """ + from Redmine import get_custom_fields_command + get_custom_fields_request_mocker = mocker.patch.object(redmine_client, 'get_custom_fields_request') + get_custom_fields_request_mocker.return_value = {"custom_fields": [{"id": "1", "name": "custom_test", "is_required": False, + "is_filter": True, "trackers": {"name": "Bug", "id": "1"} + } + ] + } + result = get_custom_fields_command(redmine_client, {}) + assert result.readable_output == ("### Custom Fields List:\n|Id|Name|IsRequired|IsFilter|Trackers|\n|---|---|---|---|---|\n" + "| 1 | custom_test | False | True | ***name***: Bug
***id***: 1 |\n") + + +def test_get_custom_fields_command_invalid_response(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-custom-field-list command is executed + Then: + - raises a response format error + """ + from Redmine import get_custom_fields_command + from CommonServerPython import DemistoException + get_custom_fields_request_mocker = mocker.patch.object(redmine_client, 'get_custom_fields_request') + get_custom_fields_request_mocker.return_value = {"custom_fieldsss": [{"id": "1", "name": "custom_test", "is_required": False, + "is_filter": True, "trackers": {"name": "Bug", "id": "1"} + } + ] + } + args = {} + with pytest.raises(DemistoException) as e: + get_custom_fields_command(redmine_client, args) + assert e.value.message == "The request succeeded, but a parse error occurred." + + +def test_get_users_command(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-user-id-list command is executed + Then: + - The http request is called with the right arguments + """ + from Redmine import get_users_command + http_request = mocker.patch.object(redmine_client, '_http_request') + get_users_command(redmine_client, {'status': 'Active'}) + http_request.assert_called_with('GET', '/users.json', params={'status': '1'}, headers={'X-Redmine-API-Key': True}) + + +def test_get_users_command_response(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-user-id-list command is executed + Then: + - The http request returns rhe right response + """ + from Redmine import get_users_command + get_users_request_mock = mocker.patch.object(redmine_client, 'get_users_request') + get_users_request_mock.return_value = {"users": [{"id": "1", "login": "admin", "admin": True, "firstname": "test", + "lastname": "response"}]} + result = get_users_command(redmine_client, {}) + assert result.readable_output == ("### Users List:\n|Id|Login|Admin|Firstname|Lastname|\n|---|---|---|---|---|\n" + "| 1 | admin | True | test | response |\n") + + +def test_get_users_command_invalid_response(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-user-id-list command is executed + Then: + - The http request is called with the right arguments + """ + from Redmine import get_users_command + from CommonServerPython import DemistoException + args = {'status_id': 'open'} + mocker.patch.object(Client, 'get_users_request', return_value={'usersss': {}}) + # Execute and assert + with pytest.raises(DemistoException) as e: + get_users_command(redmine_client, args) + assert str(e.value) == "The request succeeded, but a parse error occurred." + + +def test_get_users_command_status_invalid(mocker, redmine_client): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-user-id-list command is executed + Then: + - The http request is called with the right arguments + """ + from Redmine import get_users_command + from CommonServerPython import DemistoException + with pytest.raises(DemistoException) as e: + get_users_command(redmine_client, {'status': 'hbvkbk'}) + assert e.value.message == "Invalid status value- please use the predefined options only." + + +''' HELPER FUNCTIONS TESTS ''' + + +@pytest.mark.parametrize('page_size, page_number, expected_output', + [(1, 10, '#### Showing 1 results from page 10:\n')]) +def test_create_paging_header(page_size, page_number, expected_output): + """ + Given: + - All relevant arguments for the command that is executed + When: + - redmine-user-id-list command is executed + Then: + - The http request is called with the right arguments + """ + from Redmine import create_paging_header + assert create_paging_header(page_size, page_number) == expected_output + + +@pytest.mark.parametrize('args, expected_output', + [({'page_number': '2', 'page_size': '20'}, (20, 20, 2))]) +def test_adjust_paging_to_request(args, expected_output): + """ + Given: + - All relevant arguments for the command that is executed + When: + - adjust_paging_to_request function is executed + Then: + - The http request is called with the right arguments + """ + from Redmine import adjust_paging_to_request + assert adjust_paging_to_request(args['page_number'], args['page_size'], None) == expected_output + + +def test_convert_args_to_request_format(): + """ + Given: + - All relevant arguments for the command that is executed + When: + - convert_args_to_request_format command is executed + Then: + - The key or value is being converted + """ + from Redmine import convert_args_to_request_format + args = {'tracker_id': 'Bug'} + convert_args_to_request_format(args) + assert args['tracker_id'] == '1' + + +def test_convert_args_to_request_format_invalid(): + """ + Given: + - All relevant arguments for the command that is executed + When: + - convert_args_to_request_format command is executed + Then: + - raises prriority_id is invalid + """ + from CommonServerPython import DemistoException + from Redmine import convert_args_to_request_format + args = {'priority_id': 'lknljkl'} + with pytest.raises(DemistoException) as e: + convert_args_to_request_format(args) + assert e.value.message == "Predefined value for priority_id is not in format." diff --git a/Packs/Redmine/Integrations/Redmine/TestPlaybooks/Redmine-Test.yml b/Packs/Redmine/Integrations/Redmine/TestPlaybooks/Redmine-Test.yml new file mode 100644 index 000000000000..12501446be88 --- /dev/null +++ b/Packs/Redmine/Integrations/Redmine/TestPlaybooks/Redmine-Test.yml @@ -0,0 +1,841 @@ +id: Redmine_Test +version: -1 +name: Redmine_Test +description: Tests the Redmine Integrations command. +starttaskid: "0" +tasks: + "0": + id: "0" + taskid: 921223a8-9b77-4b39-8a05-55527d793e14 + type: start + task: + id: 921223a8-9b77-4b39-8a05-55527d793e14 + version: -1 + name: "" + iscommand: false + brand: "" + description: '' + nexttasks: + '#none#': + - "1" + separatecontext: false + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 50 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 0 + isoversize: false + isautoswitchedtoquietmode: false + "1": + id: "1" + taskid: d41600f5-1f2d-471e-8130-b476ddfffef9 + type: regular + task: + id: d41600f5-1f2d-471e-8130-b476ddfffef9 + version: -1 + name: Delete All Context + description: |- + Delete field from context. + + This automation runs using the default Limited User role, unless you explicitly change the permissions. + For more information, see the section about permissions here: + https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/6.10/Cortex-XSOAR-Administrator-Guide/Automations + scriptName: DeleteContext + type: regular + iscommand: false + brand: "" + nexttasks: + '#none#': + - "2" + scriptarguments: + all: + simple: "yes" + separatecontext: false + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 195 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 0 + isoversize: false + isautoswitchedtoquietmode: false + "2": + id: "2" + taskid: d7206441-27f8-44a0-8d01-73ee0e6288ce + type: regular + task: + id: d7206441-27f8-44a0-8d01-73ee0e6288ce + version: -1 + name: redmine-issue-create + description: Create an issue + script: '|||redmine-issue-create' + type: regular + iscommand: true + brand: "" + nexttasks: + '#none#': + - "13" + scriptarguments: + priority_id: + simple: Normal + project_id: + simple: "1" + status_id: + simple: New + subject: + simple: New Issue for Test playbook + tracker_id: + simple: Bug + separatecontext: false + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 370 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 0 + isoversize: false + isautoswitchedtoquietmode: false + "3": + id: "3" + taskid: 8dde6879-29e2-4893-8108-e023a2320668 + type: regular + task: + id: 8dde6879-29e2-4893-8108-e023a2320668 + version: -1 + name: redmine-issue-get + description: Show an issue by id. + script: '|||redmine-issue-get' + type: regular + iscommand: true + brand: "" + nexttasks: + '#none#': + - "14" + scriptarguments: + issue_id: + simple: ${Redmine.Issue.id} + separatecontext: false + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 720 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 2 + isoversize: false + isautoswitchedtoquietmode: false + "4": + id: "4" + taskid: 7ef86989-7d70-4281-8e9e-b95d2b39cbbd + type: regular + task: + id: 7ef86989-7d70-4281-8e9e-b95d2b39cbbd + version: -1 + name: redmine-issue-delete + description: Delete an issue by its ID + script: '|||redmine-issue-delete' + type: regular + iscommand: true + brand: "" + nexttasks: + '#none#': + - "23" + scriptarguments: + issue_id: + simple: ${Redmine.Issue.id} + separatecontext: false + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 2470 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 0 + isoversize: false + isautoswitchedtoquietmode: false + "5": + id: "5" + taskid: b279bfc3-ceba-4c4f-80e6-9d102f1ac4f6 + type: regular + task: + id: b279bfc3-ceba-4c4f-80e6-9d102f1ac4f6 + version: -1 + name: redmine-issue-update + description: Update an existing issue + script: '|||redmine-issue-update' + type: regular + iscommand: true + brand: "" + nexttasks: + '#none#': + - "6" + scriptarguments: + issue_id: + simple: ${Redmine.Issue.id} + subject: + simple: changed from test playbook + separatecontext: false + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 1070 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 0 + isoversize: false + isautoswitchedtoquietmode: false + "6": + id: "6" + taskid: b785ee77-ec7c-4c7c-83a8-a393e5fe4a2e + type: regular + task: + id: b785ee77-ec7c-4c7c-83a8-a393e5fe4a2e + version: -1 + name: redmine-issue-get + description: Show an issue by id. + script: '|||redmine-issue-get' + type: regular + iscommand: true + brand: "" + nexttasks: + '#none#': + - "15" + scriptarguments: + include: + simple: watchers + issue_id: + simple: ${Redmine.Issue.id} + separatecontext: false + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 1245 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 2 + isoversize: false + isautoswitchedtoquietmode: false + "7": + id: "7" + taskid: f9cb4ccc-1b7a-46f3-8156-f77011e05fda + type: regular + task: + id: f9cb4ccc-1b7a-46f3-8156-f77011e05fda + version: -1 + name: redmine-issue-watcher-add + description: Add a watcher to the specified issue + script: '|||redmine-issue-watcher-add' + type: regular + iscommand: true + brand: "" + nexttasks: + '#none#': + - "17" + scriptarguments: + issue_id: + simple: ${Redmine.Issue.id} + watcher_id: + simple: "1" + separatecontext: false + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 1595 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 0 + isoversize: false + isautoswitchedtoquietmode: false + "9": + id: "9" + taskid: f801ecfb-8f21-4ade-8f1b-0f1f3fdce95d + type: regular + task: + id: f801ecfb-8f21-4ade-8f1b-0f1f3fdce95d + version: -1 + name: redmine-user-id-list + description: Retrieve a list of users with optional filtering options. + script: '|||redmine-user-id-list' + type: regular + iscommand: true + brand: "" + nexttasks: + '#none#': + - "10" + separatecontext: false + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 2995 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 0 + isoversize: false + isautoswitchedtoquietmode: false + "10": + id: "10" + taskid: cfb79bde-6ff5-4443-8769-effb2a5c166f + type: regular + task: + id: cfb79bde-6ff5-4443-8769-effb2a5c166f + version: -1 + name: redine-custom-field-list + description: Retrieve a list of all custom fields. + script: '|||redmine-custom-field-list' + type: regular + iscommand: true + brand: "" + nexttasks: + '#none#': + - "22" + separatecontext: false + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 3170 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 0 + isoversize: false + isautoswitchedtoquietmode: false + "11": + id: "11" + taskid: 8f746c21-8fd3-43b6-8fc6-8e4e87685bd9 + type: regular + task: + id: 8f746c21-8fd3-43b6-8fc6-8e4e87685bd9 + version: -1 + name: redmine-issue-list + description: Display a list of issues + script: '|||redmine-issue-list' + type: regular + iscommand: true + brand: "" + nexttasks: + '#none#': + - "18" + scriptarguments: + limit: + simple: "1" + project_id: + simple: "1" + separatecontext: false + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 2120 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 0 + isoversize: false + isautoswitchedtoquietmode: false + "13": + id: "13" + taskid: f1bfc818-aff7-45f7-8ac4-adc12e0ec13d + type: condition + task: + id: f1bfc818-aff7-45f7-8ac4-adc12e0ec13d + version: -1 + name: Verify Outputs + type: condition + iscommand: false + brand: "" + nexttasks: + "yes": + - "3" + separatecontext: false + conditions: + - label: "yes" + condition: + - - operator: isNotEmpty + left: + value: + simple: Redmine.Issue.id + iscontext: true + - - operator: isEqualString + left: + value: + simple: Redmine.Issue.subject + iscontext: true + right: + value: + simple: New Issue for Test playbook + - - operator: isEqualString + left: + value: + simple: Redmine.Issue.priority.id + iscontext: true + right: + value: + simple: "2" + - - operator: isEqualString + left: + value: + simple: Redmine.Issue.tracker.id + iscontext: true + right: + value: + simple: "1" + - - operator: isEqualString + left: + value: + simple: Redmine.Issue.project.id + iscontext: true + right: + value: + simple: "1" + - - operator: isEqualString + left: + value: + simple: Redmine.Issue.status.id + iscontext: true + right: + value: + simple: "1" + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 545 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 0 + isoversize: false + isautoswitchedtoquietmode: false + "14": + id: "14" + taskid: fbb380de-3cc8-4525-835d-ec1ec045d596 + type: condition + task: + id: fbb380de-3cc8-4525-835d-ec1ec045d596 + version: -1 + name: Verify Outputs + type: condition + iscommand: false + brand: "" + nexttasks: + "yes": + - "5" + separatecontext: false + conditions: + - label: "yes" + condition: + - - operator: isEqualString + left: + value: + simple: Redmine.Issue.id + iscontext: true + right: + value: + simple: Redmine.Issue.id + iscontext: true + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 895 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 0 + isoversize: false + isautoswitchedtoquietmode: false + "15": + id: "15" + taskid: 90abf1c7-89a1-4968-87d5-44d545ccb9c2 + type: condition + task: + id: 90abf1c7-89a1-4968-87d5-44d545ccb9c2 + version: -1 + name: Verify outputs + type: condition + iscommand: false + brand: "" + nexttasks: + "yes": + - "7" + separatecontext: false + conditions: + - label: "yes" + condition: + - - operator: isEqualString + left: + value: + simple: Redmine.Issue.subject + iscontext: true + right: + value: + simple: changed from test playbook + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 1420 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 0 + isoversize: false + isautoswitchedtoquietmode: false + "16": + id: "16" + taskid: 2d4a0152-8349-4d16-892d-f4e5e24644a9 + type: condition + task: + id: 2d4a0152-8349-4d16-892d-f4e5e24644a9 + version: -1 + name: Verify outputs + type: condition + iscommand: false + brand: "" + nexttasks: + "yes": + - "11" + separatecontext: false + conditions: + - label: "yes" + condition: + - - operator: containsGeneral + left: + value: + simple: Redmine.Issue.watchers.id + iscontext: true + right: + value: + simple: "1" + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 1945 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 0 + isoversize: false + isautoswitchedtoquietmode: false + "17": + id: "17" + taskid: 709e4a5f-8adc-4bd8-8e3a-7340a09038a0 + type: regular + task: + id: 709e4a5f-8adc-4bd8-8e3a-7340a09038a0 + version: -1 + name: redmine-issue-get + description: Show an issue by id. + script: '|||redmine-issue-get' + type: regular + iscommand: true + brand: "" + nexttasks: + '#none#': + - "16" + scriptarguments: + include: + simple: watchers + issue_id: + simple: ${Redmine.Issue.id} + separatecontext: false + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 1770 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 2 + isoversize: false + isautoswitchedtoquietmode: false + "18": + id: "18" + taskid: 921f71c1-100d-4f39-842a-17518397485f + type: condition + task: + id: 921f71c1-100d-4f39-842a-17518397485f + version: -1 + name: Verify Outputs + type: condition + iscommand: false + brand: "" + nexttasks: + "yes": + - "4" + separatecontext: false + conditions: + - label: "yes" + condition: + - - operator: containsGeneral + left: + value: + simple: Redmine.Issue.id + iscontext: true + right: + value: + simple: Redmine.Issue.id + iscontext: true + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 2295 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 0 + isoversize: false + isautoswitchedtoquietmode: false + "21": + id: "21" + taskid: 4c245216-5636-415b-8066-1f652268e89f + type: title + task: + id: 4c245216-5636-415b-8066-1f652268e89f + version: -1 + name: End of Playbook + type: title + iscommand: false + brand: "" + description: '' + separatecontext: false + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 3520 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 0 + isoversize: false + isautoswitchedtoquietmode: false + "22": + id: "22" + taskid: 6c55e194-56e8-4dc8-869b-f0bcefb8a21e + type: regular + task: + id: 6c55e194-56e8-4dc8-869b-f0bcefb8a21e + version: -1 + name: Delete All context + description: |- + Delete field from context. + + This automation runs using the default Limited User role, unless you explicitly change the permissions. + For more information, see the section about permissions here: + https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/6.10/Cortex-XSOAR-Administrator-Guide/Automations + scriptName: DeleteContext + type: regular + iscommand: false + brand: "" + nexttasks: + '#none#': + - "21" + scriptarguments: + all: + simple: "yes" + separatecontext: false + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 3345 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 0 + isoversize: false + isautoswitchedtoquietmode: false + "23": + id: "23" + taskid: 8be4b962-8ead-47e0-8b6d-3d3750a6dbae + type: regular + task: + id: 8be4b962-8ead-47e0-8b6d-3d3750a6dbae + version: -1 + name: Check if issue was deleted + description: Show an issue by id + script: '|||redmine-issue-get' + type: regular + iscommand: true + brand: "" + nexttasks: + '#error#': + - "24" + scriptarguments: + issue_id: + simple: ${Redmine.Issue.id} + separatecontext: false + continueonerror: true + continueonerrortype: errorPath + view: |- + { + "position": { + "x": 50, + "y": 2645 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 0 + isoversize: false + isautoswitchedtoquietmode: false + "24": + id: "24" + taskid: 9b57bd34-e218-4ca0-8de3-9213a393f660 + type: regular + task: + id: 9b57bd34-e218-4ca0-8de3-9213a393f660 + version: -1 + name: Delete All Context + description: |- + Delete field from context. + + This automation runs using the default Limited User role, unless you explicitly change the permissions. + For more information, see the section about permissions here: + https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/6.10/Cortex-XSOAR-Administrator-Guide/Automations + scriptName: DeleteContext + type: regular + iscommand: false + brand: "" + nexttasks: + '#none#': + - "9" + scriptarguments: + all: + simple: "yes" + separatecontext: false + continueonerrortype: "" + view: |- + { + "position": { + "x": 50, + "y": 2820 + } + } + note: false + timertriggers: [] + ignoreworker: false + skipunavailable: false + quietmode: 0 + isoversize: false + isautoswitchedtoquietmode: false +view: |- + { + "linkLabelsPosition": {}, + "paper": { + "dimensions": { + "height": 3535, + "width": 380, + "x": 50, + "y": 50 + } + } + } +inputs: [] +outputs: [] +fromversion: 6.10.0 diff --git a/Packs/Redmine/Integrations/Redmine/command_examples b/Packs/Redmine/Integrations/Redmine/command_examples new file mode 100644 index 000000000000..6b9081da2914 --- /dev/null +++ b/Packs/Redmine/Integrations/Redmine/command_examples @@ -0,0 +1,10 @@ +!redmine-issue-create priority_id=High status_id=Closed subject=helloExample tracker_id=Bug project_id=1 watcher_user_ids=5,6 custom_fields=1:helloCustom +!redmine-issue-update issue_id=130 subject=subjectChanged +!redmine-issue-get issue_id=130 include=watchers +!redmine-issue-watcher-add issue_id=130 watcher_id=1 +!redmine-issue-watcher-remove issue_id=130 watcher_id=1 +!redmine-issue-list limit=2 +!redmine-project-list +!redmine-custom-field-list +!redmine-user-id-list +!redmine-issue-delete issue_id=130 diff --git a/Packs/Redmine/README.md b/Packs/Redmine/README.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Packs/Redmine/pack_metadata.json b/Packs/Redmine/pack_metadata.json new file mode 100644 index 000000000000..14dbbf3ce504 --- /dev/null +++ b/Packs/Redmine/pack_metadata.json @@ -0,0 +1,20 @@ +{ + "name": "Redmine", + "description": "A project management and issue tracking system.", + "support": "xsoar", + "currentVersion": "1.0.0", + "author": "Cortex XSOAR", + "url": "https://www.paloaltonetworks.com/cortex", + "email": "", + "created": "2024-02-13T00:00:00Z", + "categories": [ + "Utilities" + ], + "tags": [], + "useCases": [], + "keywords": [], + "marketplaces": [ + "xsoar", + "marketplacev2" + ] +} \ No newline at end of file diff --git a/Tests/conf.json b/Tests/conf.json index 578cce056647..4aff201e3b8e 100644 --- a/Tests/conf.json +++ b/Tests/conf.json @@ -5646,6 +5646,10 @@ "integrations": "AWS-SNS-Listener", "playbookID": "AWS SNS Listener - Test", "instance_names": "AWS-SNS-Listener" + }, + { + "integrations": "Redmine", + "playbookID": "Redmine-Test" } ], "skipped_tests": {