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
| ID | Redmine |
|---|---|
| Provider | Open Source |
| Category | Utilities |
| From Version | 6.10.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix 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
- Navigate to Settings > Integrations > Servers & Services.
- Search for Redmine.
-
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 - 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
- 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: helloCustom2024-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: helloCustom2024-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: helloCustom2024-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: Support3 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 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 Idinsecure— Trust any certificate (not secure)
Commands (13)
-
redmine-custom-field-listRetrieve 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-deleteDelete an issue by its ID.
-
redmine-issue-getShow an issue by ID.
-
redmine-issue-listDisplay a list of issues.
-
redmine-issue-updateUpdate 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-addAdd 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-removeRemove a watcher of an issue.
-
redmine-priority-id-listReturns a list of priorities (priority name to priority id).
-
redmine-project-listRetrieve a list of all projects, including both public and private ones that the user has access to.
-
redmine-status-id-listReturns a list of statuses (status name to status id).
-
redmine-tracker-id-listReturns 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()