diff --git a/responders/Redmine/Redmine_Issue.json b/responders/Redmine/Redmine_Issue.json new file mode 100644 index 000000000..a4269a003 --- /dev/null +++ b/responders/Redmine/Redmine_Issue.json @@ -0,0 +1,88 @@ +{ + "name": "Redmine_Issue", + "version": "1.0", + "author": "Marc-André DOLL", + "url": "https://github.com/TheHive-Project/Cortex-Analyzers", + "license": "AGPL-V3", + "description": "Create a redmine issue from a case", + "dataTypeList": [ + "thehive:case", + "thehive:case_task" + ], + "command": "Redmine/redmine.py", + "baseConfig": "Redmine", + "configurationItems": [ + { + "name": "instance_name", + "description": "Name of the Redmine instance", + "multi": false, + "required": false, + "type": "string", + "defaultValue": "redmine" + }, + { + "name": "url", + "description": "URL where to find the Redmine API", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "username", + "description": "Username to log into Redmine", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "password", + "description": "Password to log into Redmine", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "project_field", + "description": "Name of the custom field containing the Redmine project to use when creating the issue", + "multi": false, + "required": true, + "type": "string" + }, + { + "name": "tracker_field", + "description": "Name of the custom field containing the Redmine tracker to use when creating the issue", + "multi": false, + "required": true, + "type": "string" + }, + { + "name": "assignee_field", + "description": "Name of the custom field containing the Redmine assignee to use when creating the issue", + "multi": false, + "required": false, + "type": "string" + }, + { + "name": "reference_field", + "description": "Name of the case custom field in which to store the opened issue. If not defined, this information will not be stored", + "type": "string", + "required": false, + "multi": false + }, + { + "name": "opening_status", + "description": "Status used when opening a Redmine issue (if not defined here, will use the default opening status from the Redmine Workflow)", + "type": "string", + "multi": false, + "required": false + }, + { + "name": "closing_task", + "description": "Closing the task after successfully creating the Redmine issue", + "type": "boolean", + "multi": false, + "defaultValue": false, + "required": false + } + ] +} diff --git a/responders/Redmine/redmine.py b/responders/Redmine/redmine.py new file mode 100755 index 000000000..b5950791f --- /dev/null +++ b/responders/Redmine/redmine.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# encoding: utf-8 + +from cortexutils.responder import Responder +import redmine_client + +class Redmine(Responder): + def __init__(self): + Responder.__init__(self) + self.instance_name = self.get_param('config.instance_name', 'redmine') + self.instance_url = self.get_param('config.url', None, 'Missing Redmine URL') + self.client = redmine_client.RedmineClient( + baseurl=self.instance_url, + username=self.get_param('config.username', None, 'Missing username'), + password=self.get_param('config.password', None, 'Missing password')) + self.project_field = self.get_param('config.project_field', None, 'Missing custom field for Redmine project') + self.tracker_field = self.get_param('config.tracker_field', None, 'Missing custom field for Redmine tracker') + self.assignee_field = self.get_param('config.assignee_field', None, 'Missing custom field for Redmine assignee') + self.reference_field = self.get_param('config.reference_field', None) + self.closing_task = self.get_param('config.closing_task', False) + + def run(self): + issue_data = {} + if self.data_type == 'thehive:case': + issue_data = self.extract_case_data() + elif self.data_type == 'thehive:case_task': + issue_data = self.extract_case_data('data.case') + else: + self.error('Invalid dataType') + try: + issue = self.client.create_issue( + title=issue_data['title'], body=issue_data['description'], + project=issue_data['project'], tracker=issue_data['tracker'], + status=issue_data['status'], priority=issue_data['severity'], + assignee=issue_data['assignee']) + self.report({ + 'message': 'issue {} created'.format(issue['issue']['id']), + 'instance': {'name': self.instance_name, "url": self.instance_url}, + 'issue': issue + }) + except Exception as e: + self.error(str(e)) + + def operations(self, raw): + ops = [] + if self.reference_field: + ops.append(self.build_operation('AddCustomFields', name=self.reference_field, tpe='string', value='{}#{}'.format(self.instance_name, raw['issue']['issue']['id']))) + if self.data_type == 'thehive:case_task' and self.closing_task: + ops.append(self.build_operation('CloseTask')) + return ops + + def extract_case_data(self, data_root='data'): + issue_data = {} + issue_data['title'] = self.get_param('{}.title'.format(data_root), None, 'Case title is missing') + issue_data['description'] = self.get_param('{}.description'.format(data_root), None, 'Case description is missing') + issue_data['severity'] = self.get_param('{}.severity'.format(data_root)) + if self.project_field: + issue_data['project'] = self.get_param('{}.customFields.{}.string'.format(data_root, self.project_field), None, 'Project not defined in case') + if self.tracker_field: + issue_data['tracker'] = self.get_param('{}.customFields.{}.string'.format(data_root, self.tracker_field), None, 'Tracker not defined in case') + if self.assignee_field: + issue_data['assignee'] = self.get_param('{}.customFields.{}.string'.format(data_root, self.assignee_field), None) + issue_data['status'] = self.get_param('config.opening_status') + return issue_data + +if __name__ == '__main__': + Redmine().run() diff --git a/responders/Redmine/redmine_client.py b/responders/Redmine/redmine_client.py new file mode 100644 index 000000000..2685a6923 --- /dev/null +++ b/responders/Redmine/redmine_client.py @@ -0,0 +1,77 @@ +import requests + +class RedmineClient: + def __init__(self, baseurl, username, password): + self.base_url = baseurl + self.session = requests.Session() + self.session.headers.update({'content-type': 'application/json'}) + self.session.auth = (username, password) + + def create_issue(self, title=None, body=None, project=None, tracker=None, + priority=None, status=None, assignee=None): + payload = { + 'issue': { + 'subject': title, + 'description': body, + 'project_id': project, + 'tracker_id': self.get_tracker_id(tracker), + 'priority_id': priority, + 'status_id': self.get_status_id(status), + 'assigned_to_id': self.get_assignee_id(project, assignee) + } + } + url = self.base_url + '/issues.json' + response = self.session.post(url, json=payload) + response.raise_for_status() + result = response.json() + if 'error' in result: + raise RedmineClientError(result['error']) + return result + + def get_tracker_id(self, name): + url = self.base_url + '/trackers.json' + id = None + trackers = self.session.get(url) + trackers.raise_for_status() + for p in trackers.json()['trackers']: + if p['name'] == name: + id = p['id'] + break + return id + + def get_status_id(self, name): + url = self.base_url + '/issue_statuses.json' + id = None + issue_statuses = self.session.get(url) + issue_statuses.raise_for_status() + for p in issue_statuses.json()['issue_statuses']: + if p['name'] == name: + id = p['id'] + break + return id + + def get_assignee_id(self, project, assignee): + url = '{}/projects/{}/memberships.json'.format(self.base_url, project) + id = None + payload = {'offset': 0} + total_count = 0 + while id is None and payload['offset'] <= total_count: + response = self.session.get(url, params=payload) + response.raise_for_status() + for member in response.json()['memberships']: + if 'user' in member: + if assignee == member['user']['name']: + id = member['user']['id'] + break + elif 'group' in member: + if assignee == member['group']['name']: + id = member['group']['id'] + break + total_count = response.json()['total_count'] + payload['offset'] += response.json()['limit'] + return id + + +class RedmineClientError(Exception): + def __init__(self, message): + self.message = message diff --git a/responders/Redmine/requirements.txt b/responders/Redmine/requirements.txt new file mode 100644 index 000000000..2cac23c03 --- /dev/null +++ b/responders/Redmine/requirements.txt @@ -0,0 +1,2 @@ +requests +cortexutils