Redmine

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.

Utilities · Redmine

Details

IDRedmine
ProviderOpen Source
CategoryUtilities
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

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., https://1.1.1.1) 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.
  • The status of the newly created issue will be set to the default status of the Redmine instance.
  • When attaching a file to an issue, include the entry ID in the request as file_entry_id=entry ID.
  • 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 the instance configuration will be used. Optional
tracker_id - A tracker ID (e.g., 1,2,3).
- Get the tracker_name to tracker_id mapping by using the command redmine-tracker-id-list.
- If an invalid ID was entered, the default tracker ID set in the Redmine instance wil be used.
Required
priority_id - A priority ID (e.g., 1,2,3).
- Get the priority_name to priority_id mapping by using the command redmine-priority-id-list.
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 {\"customFieldID2\": \"value3\", \"customFieldID1\": [\"value1\",\"value2\"]}.”
- Use an array if the field is of multiselect type.
- Instruction for each custom field type:
- Boolean: use “0” for False and “1” for True.
- Date: the format is “yyyy-mm-dd”.
- Float: A number with two digits after the decimal point.
- Integer: A number.
- Key/Value list: Use the Key (ID) of the value.
- User: Use the user ID.
- Version- use the version ID.
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 subject=helloExample tracker_id=1 project_id=1 watcher_user_ids=5,6 custom_fields=1:helloCustom priority_id=1

Context Example

{
    "Redmine": {
        "Issue": {
            "author": {
                "id": 6,
                "name": "Integration Test"
            },
            "closed_on": null,
            "created_on": "2024-04-07T11:59:34Z",
            "custom_fields": [
                {
                    "id": 1,
                    "name": "Team_of_workers",
                    "value": "helloCustom"
                }
            ],
            "description": null,
            "done_ratio": 0,
            "due_date": null,
            "estimated_hours": null,
            "id": "167",
            "is_private": false,
            "priority": {
                "id": 1,
                "name": "Low"
            },
            "project": {
                "id": 1,
                "name": "Cortex XSOAR"
            },
            "start_date": "2024-04-07",
            "status": {
                "id": 1,
                "is_closed": false,
                "name": "New"
            },
            "subject": "helloExample",
            "total_estimated_hours": null,
            "tracker": {
                "id": 1,
                "name": "Bug"
            },
            "updated_on": "2024-04-07T11:59:34Z"
        }
    }
}

Human Readable Output

The issue you created

Id Project Tracker Status Priority Author Created On Subject Start Date Custom Fields
167 Cortex XSOAR Bug New Low Integration Test 2024-04-07T11:59:34Z helloExample 2024-04-07 - 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 instance 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 (e.g., 1,2,3).
- Get the tracker_name to tracker_id mapping by using the command redmine-tracker-id-list.
Optional
status - A status ID (e.g., 1,2,3) or use predefined values.
- Possible values: Open/Closed/All from the predefined values or use any of your custom status ids (e.g., 1).
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 which was 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

{
    "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=entry ID. 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 - A tracker ID (e.g., 1,2,3).
- Get the tracker_name to tracker_id mapping by using the command redmine-tracker-id-list.
- If an invalid ID was entered, tracker_id won’t be changed and won’t raise an error.
Optional
priority_id - A priority ID (e.g., 1,2,3).
- Get the priority_name to priority_id mapping by using the command redmine-priority-id-list.
Optional
status - A status ID (e.g., 1,2,3).
- Get the status_name to status_id mapping by using the command redmine-status-id-list.
- If an invalid ID was entered, status_id won’t be changed and won’t raise an error.
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 {\"customFieldID2\": \"value3\", \"customFieldID1\": [\"value1\",\"value2\"]}.”
- Use an array if the field is of multiselect type.
- Instruction for each custom field type:
- Boolean: use “0” for False and “1” for True.
- Date: the format is “yyyy-mm-dd”.
- Float: A number with two digits after the decimal point.
- Integer: A number.
- Key/Value list: Use the Key (ID) of the value.
- User: Use the user ID.
- Version- use the version ID.
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 wanted issue. 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 str The ID of the found 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.
Redmine.Issue.watchers.id str The watchers of the issue.

Command example

!redmine-issue-get issue_id=130 include=watchers

Context Example

{
    "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 about to be deleted issue. 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 Array Display a list of projects accessible to the user.

Command example


#### 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


#### 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


#### 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

redmine-priority-id-list


Returns a list of priorities (priority_name to priority_id).

Base Command

redmine-priority-id-list

Input

There are no input arguments for this command.

Context Output

Path Type Description
Redmine.priorities.ID str ID of the priority field.
Redmine.priorities.Name str Name of the priority field.

Command example


#### Context Example

```json
{
    "Redmine": {
        "priorities": [
            {
                "ID": 1,
                "Name": "Low"
            },
            {
                "ID": 2,
                "Name": "Normal"
            },
            {
                "ID": 3,
                "Name": "High"
            },
            {
                "ID": 4,
                "Name": "Urgent"
            },
            {
                "ID": 5,
                "Name": "Immediate"
            }
        ]
    }
}

Human Readable Output

priorities name to id List

ID Name
1 Low
2 Normal
3 High
4 Urgent
5 Immediate

redmine-status-id-list


Returns a list of statuses (status_name to status_id).

Base Command

redmine-status-id-list

Input

There are no input arguments for this command.

Context Output

Path Type Description
Redmine.statuses.ID str ID of the status field.
Redmine.Users.Name str Name of the status field.

Command example


#### Context Example

```json
{
    "Redmine": {
        "statuses": [
            {
                "ID": 1,
                "Name": "help"
            },
            {
                "ID": 2,
                "Name": "In Progress"
            },
            {
                "ID": 3,
                "Name": "Resolved"
            },
            {
                "ID": 4,
                "Name": "Feedback"
            },
            {
                "ID": 5,
                "Name": "Closed"
            },
            {
                "ID": 6,
                "Name": "Rejected"
            }
        ]
    }
}

Human Readable Output

statuses name to id List

ID Name
1 help
2 In Progress
3 Resolved
4 Feedback
5 Closed
6 Rejected

redmine-tracker-id-list


Returns a list of trackers (tracker_name to tracker_id).

Base Command

redmine-tracker-id-list

Input

There are no input arguments for this command.

Context Output

Path Type Description
Redmine.trackers.ID str ID of the tracker field.
Redmine.trackers.Name str Name of the tracker field.

Command example


#### Context Example

```json
{
    "Redmine": {
        "trackers": [
            {
                "ID": 1,
                "Name": "Bug111"
            },
            {
                "ID": 2,
                "Name": "Feature"
            },
            {
                "ID": 3,
                "Name": "Support"
            }
        ]
    }
}

Human Readable Output

trackers name to id List

ID Name
1 Bug111
2 Feature
3 Support

Configuration parameters

  • url — Server URL (e.g., https://1.1.1.1) (required)
  • credentials — (required)
  • project_id — Project Id
  • insecure — Trust any certificate (not secure)

Commands (13)

  • redmine-custom-field-list

    Retrieve a list of all custom fields.

  • redmine-issue-create

    - Create a new issue. - The status of the newly created issue will be set to the default status of the Redmine instance. - When attaching a file to an issue, include the entry ID in the request as file_entry_id=entry ID. - 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.

  • redmine-issue-delete

    Delete an issue by its ID.

  • redmine-issue-get

    Show an issue by ID.

  • redmine-issue-list

    Display a list of issues.

  • 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.

  • redmine-issue-watcher-add

    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.

  • redmine-issue-watcher-remove

    Remove a watcher of an issue.

  • redmine-priority-id-list

    Returns a list of priorities (priority name to priority id).

  • redmine-project-list

    Retrieve a list of all projects, including both public and private ones that the user has access to.

  • redmine-status-id-list

    Returns a list of statuses (status name to status id).

  • redmine-tracker-id-list

    Returns a list of trackers (tracker name to tracker id).

  • redmine-user-id-list

    - Retrieve a list of users with optional filtering options. - This command requires admin privileges in your Redmine account.

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 = (
    "Error. This may be due to 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_STATUS_FOR_LIST_COMMAND = {"Open": "open", "Closed": "closed", "All": "*"}

DICT_OF_ISSUE_ARGS = {
    "trackers": {"key_in_response": "trackers", "url_suffix": "/trackers.json", "singular": "tracker"},
    "statuses": {"key_in_response": "issue_statuses", "url_suffix": "/issue_statuses.json", "singular": "status"},
    "priorities": {
        "key_in_response": "issue_priorities",
        "url_suffix": "/enumerations/issue_priorities.json",
        "singular": "priority",
    },
}
""" 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, 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(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

    def field_from_name_to_id_list_request(self, field):
        response = self._http_request("GET", DICT_OF_ISSUE_ARGS.get(field, {}).get("url_suffix", ""), 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 format_custom_field_to_request(args: Dict[str, Any]):
    if custom_fields := args.pop("custom_fields", ""):
        try:
            custom_fields_for_request = []
            custom_fields_dict = json.loads(custom_fields)
            for key, value in custom_fields_dict.items():
                custom_fields_for_request.append({"id": key, "value": value})
            args["custom_fields"] = custom_fields_for_request
        except Exception as e:
            raise DemistoException(
                f"Custom fields not in format, please follow this format:"
                f' `{{"customFieldID2": "value3", "customFieldID1": ["value1","value2"]}}` - Please use an array if '
                f"the field is of multiselect type. with error: {e}"
            )


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(f"Could not create a token for your file- please try again. 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
    format_custom_field_to_request(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(f"{INVALID_ID_DEMISTO_ERROR} With error: {e}")
        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)
    format_custom_field_to_request(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(f"{INVALID_ID_DEMISTO_ERROR} With error: {e}")
        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 format_and_validate_fields_to_request(custom_field: str, status: str):
        if custom_field:
            try:
                cf_in_format = custom_field.split(":", 1)
                args[f"cf_{cf_in_format[0]}"] = cf_in_format[1]
            except Exception as e:
                if "list index out of range" in e.args[0] or "substring not found" in e.args[0]:
                    raise DemistoException(f"Invalid custom field format, please follow the command description. Error: {e}.")
                raise
        # If the user used predefined values, convert to values accepted by the API using ISSUE_STATUS_FOR_LIST_COMMAND
        status_id = ISSUE_STATUS_FOR_LIST_COMMAND.get(status)
        # If the user did not used predefined values, make sure it is a number
        if not status_id:
            status_id = status
            try:
                arg_to_number(status_id)
            except ValueError as e:
                raise DemistoException(
                    f"Status argument has to be a predefined value (Open/Closed/All) or a valid status ID but "
                    f"got {status_id}. with error: {e}."
                )
        return status_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 = args.pop("status", "Open")
    custom_field = args.pop("custom_field", None)
    status_id = format_and_validate_fields_to_request(custom_field, status)
    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, 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(f"{INVALID_ID_DEMISTO_ERROR} With error: {e}")
        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(f"{INVALID_ID_DEMISTO_ERROR} With error: {e}")
            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(f"{INVALID_ID_DEMISTO_ERROR} With error: {e}")
        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(f"{INVALID_ID_DEMISTO_ERROR} With error: {e}")
        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(f"{INVALID_ID_DEMISTO_ERROR} With error: {e}")
        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 field_from_name_to_id_list_command(client, field: str):
    response = client.field_from_name_to_id_list_request(field)
    key_in_response = DICT_OF_ISSUE_ARGS.get(field, {}).get("key_in_response", "")
    if key_in_response not in response:
        singular = DICT_OF_ISSUE_ARGS.get(field, {}).get("singular", "")
        raise DemistoException(f"Failed to retrieve {singular} IDs due to a parsing error.")
    mapping_name_to_id_of_field = [{"ID": item["id"], "Name": item["name"]} for item in response[key_in_response]]
    command_results = CommandResults(
        outputs_prefix=f"Redmine.{field}",
        outputs_key_field="ID",
        outputs=mapping_name_to_id_of_field,
        raw_response=mapping_name_to_id_of_field,
        readable_output=tableToMarkdown(
            f"{field} name to id List:",
            mapping_name_to_id_of_field,
            headers=["ID", "Name"],
            removeNull=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 == "redmine-status-id-list":
            return_results(field_from_name_to_id_list_command(client, "statuses"))
        elif command == "redmine-priority-id-list":
            return_results(field_from_name_to_id_list_command(client, "priorities"))
        elif command == "redmine-tracker-id-list":
            return_results(field_from_name_to_id_list_command(client, "trackers"))
        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()