From fba2c76b176e939f20efca829d6030ba7ef8be0a Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Thu, 10 Oct 2019 08:34:17 -0500 Subject: [PATCH 1/2] Add v1 files --- responders/RT4/README.md | 176 +++++++++ responders/RT4/__init__.py | 3 + responders/RT4/config.py | 95 +++++ responders/RT4/requirements.txt | 4 + responders/RT4/rt4.json | 91 +++++ responders/RT4/rt4.py | 387 +++++++++++++++++++ responders/RT4/template.py | 32 ++ responders/RT4/templates/malware.j2 | 24 ++ responders/RT4/templates/phishing_generic.j2 | 24 ++ 9 files changed, 836 insertions(+) create mode 100644 responders/RT4/README.md create mode 100644 responders/RT4/__init__.py create mode 100644 responders/RT4/config.py create mode 100644 responders/RT4/requirements.txt create mode 100644 responders/RT4/rt4.json create mode 100644 responders/RT4/rt4.py create mode 100644 responders/RT4/template.py create mode 100644 responders/RT4/templates/malware.j2 create mode 100644 responders/RT4/templates/phishing_generic.j2 diff --git a/responders/RT4/README.md b/responders/RT4/README.md new file mode 100644 index 000000000..602640276 --- /dev/null +++ b/responders/RT4/README.md @@ -0,0 +1,176 @@ +# Request Tracker 4 Cortex Responder +Summary: Creates RT tickets from TheHive + +Applies To: Case Observables (Artifacts) + +## Initial Responder Configuration + +The following need to be configured under **Organization --> Responders** prior to use: + +`server` - **Required** - RT4 base URL, e.g.: https://rt.domain.local + +`username` - **Required** - RT4 username for API authentication + +`password` - **Required** - RT4 password for user account above + +`Queue` - **Required** - Default queue in which to create new tickets (can be overriden by custom tag on observables) + +`Owner` - Default owner to assign newly created tickets (Optional - can be overriden by custom tags per observable) + +`Status` - Default status to assign newly created tickets (Optional - can be overriden by custom tags per observable) + +`custom_field_list` - Colon-separated Name:Value pairs of RT custom fields and values to set across all newly-created tickets (Optional - can be overriden by custom tags per observable) - adding a value of `How Reported:TheHive` would set the custom field named `How Reported` to `TheHive` on all newly created tickets + +`tag_to_template_map` - **Required** - Tags to Templates mapping (can be overriden by custom tag on observables). Should be colon-separated tag-to-template values. E.g. + +`thehive_cf_rtticket` - Name of a case custom field in TheHive in which RT ticket #s will be saved upon successful case-level Responder run (Optional - TheHive Custom Field should be of type 'String') + +`thehive_url` - TheHive Base URL, e.g., https://thehive.domain.local:9000 (Optional - only needed to process Cases) + +`thehive_token` - TheHive API token for authentication (Optional - only needed to process Cases) + +``` + +phishing:phishing_generic +spear_phishing:phishing_spear + +``` + +Any observable with a `phishing` tag would be assigned the template named `phishing_generic`. Any observale tagged `spear_phishing` would have its ticket created with a body from the `phishing_spear` template. + +## Workflow + +1. Set [Initial Responder Configuration](#Initial-Responder-Configuration) +2. [Create Template(s)](#Templates) +3. As new observables arrive, appropriately [tag](#Tags-to-Modify-RT4-Responder-Behavior) them +4. Run the RT4-CreateTicket responder +5. When complete, the ticket(s) should be created and the `thehive_cf_rtticket` custom field on TheHive cases (if present) should be populated with the URL to any created ticket + +## Templates + +Inside the `./templates` dir of the RT4 responder, you will need to create the templates for subjects and notification bodies that will be used on ticket creation. For the above example on an observable tagged to use the `phishing_generic` template, there should be a file inside ./templates/ called `phishing_generic.j2` (all templates should end in the .j2 extension since it uses Jinja2 templating) + +The .j2 files should be formatted like so: + +``` +{% block Subject %} +[SOC] ** Notification ** Phishing Site Targeting Your Organization +{% endblock %} + + +{% block Text %} +Greetings, + +We have recently discovered a potential phishing site targeting employees at your organization: + +Domain(s): +{{ indicator_list }} + +On behalf of the SOC, + +-- +soc@org.local +24x7 Watch Desk +https://www.org.local +{% endblock %} + +``` + +The mandatory blocks are `Subject` and `Text` inside which are the respective content for the ticket creation. You may reference any variables inside the template file which exist in the observable/artifact/alert/case for population of other data within the ticket notification (in the above case, ``indicator_list``). Those variables should be inside double curly-braces as is the format for Jinja. Example data available in the [Observable Object Data](#Observable-Object-Data) section. + +Inside the jinja2 template, all block names are passed at RT ticket variables with their respective block values upon ticket creation. Therefore, any number of blocks corresponding to RT fields can also be assigned to further customize setting ticket variables at the template level. + +*Example*: + +`{% block CF_Classification %}Phishing{% endblock %}` + +Every ticket created from that template will have the RT custom field CF_Classification set to "Phishing" upon ticket creation. + +## Tags to Modify RT4 Responder Behavior + +Set any of the following tags to modify behavior of the created ticket: + +`rt4_set_requestor:customer@domain.local` or `contact:customer@domain.local` - **Required** - This is the only tag that must be present. Without one of these, the ticket won't be created. + +`rt4_set_cf_Classification:phishing` - sets the CF.{Classification} = 'phishing' in RT ticket + +`rt4_set_cc:staff@domain.local` - adds staff@domain.local as Cc on ticket + +`rt4_set_admincc:emp@domain.local` - sets AdminCc of ticket to emp@domain.local + +`rt4_set_owner:staff@domain.local` - sets Owner of ticket to staff@domain.local (**must match person in RT or ticket creation will fail**) + +`rt4_set_queue:Incident Reports` - sets Queue of ticket created to _Incident Reports_ + +`rt4_set_subject:This is a test` - overrides the Subject line from the template with _This is a test_ + +`rt4_set_status:Resolved` - creates the ticket and then sets its status to _Resolved_ (can also use any other ticket status in your RT instance) + +`rt4_set_template:phishing_generic` - overrides any default template from tag_to_template_map setting when constructing the body of the notification, in this case instructing the Responder to use the `phishing_generic` template + +## Ticket customization order + +As already alluded to, there are 4 ways to customize ticket creation options: + +1. Global level + - Queue + - Owner + - Status + - Custom Fields + - Template +2. Template level + - All of the above except Template, plus: + - Requestor/Cc/AdminCc +3. Case/Alert level + - All RT options +4. Case artifact/observable level + - All RT options + +Greater numbered config options take precedence over smaller ones. + +*Example:* + +If a tag_to_template map at the Org Responder config in Cortex is set to map tags of `phishing` to the `phishing_generic` template, but a `set_rt4_template:phishing_spear` tag on the observable sets a different template, the observable tag takes precedence. + +## Observable Object Data + +Observables are a custom dictionary in which their properties are stored. In addition to the ticket properties passed to RT, each observable is also tagged with its case/artifact info which makes available the following info in each observable: + +``` +"owner": "michael", + "severity": 2, + "_routing": "AWxyhvveZCXO8BqIWSLs", + "flag": false, + "updatedBy": "michael", + "customFields": { + "RTTicket": { + "string": "http://192.168.0.2/Ticket/Display.html?id=141, http://192.168.0.2/Ticket/Display.html?id=142, http://192.168.0.2/Ticket/Display.html?id=143" + } + }, + "_type": "case", + "description": "test", + "title": "RT-testing", + "tags": [ + "contact:requestor@domain.tld", + "rt4:submitted" + ], + "createdAt": 1565289544365, + "_parent": null, + "createdBy": "michael", + "caseId": 1, + "tlp": 2, + "metrics": { + "seen_prior": 1 + }, + "_id": "AWxyhvveZCXO8BqIWSLs", + "id": "AWxyhvveZCXO8BqIWSLs", + "_version": 45, + "startDate": 1565289480000, + "pap": 2, + "status": "Open", + "updatedAt": 1570482005825, + "indicator_list": [ + "malicious.baddomain.tld" + ] +``` +Those properties can all be referenced as variables in the jinja2 template as mentioned in the [Templates section](#Templates). diff --git a/responders/RT4/__init__.py b/responders/RT4/__init__.py new file mode 100644 index 000000000..0785ed80f --- /dev/null +++ b/responders/RT4/__init__.py @@ -0,0 +1,3 @@ +""" +Allow imports from this dir +""" diff --git a/responders/RT4/config.py b/responders/RT4/config.py new file mode 100644 index 000000000..c3ce25461 --- /dev/null +++ b/responders/RT4/config.py @@ -0,0 +1,95 @@ +# Config item classes + +class RT4ResponderConfig(dict): + """Define what an RT4 Responder Config should allow and how it can be set (dict + that only takes certain keys). + Format courtesy of: https://stackoverflow.com/a/8187408 and https://stackoverflow.com/a/40631881 + + Configs should be init'd like so: config = RT4ResponderConfig(1, **data) where 1 = weight/rank and data is a dict of k,v's + Configs should be updated like so: config.update(1, **newdata) where 1 = weight/rank and newdata is a dict of k,v's. In this + case, the newdata would not be entered since its weight is not greater than the existing data. + """ + + def __init__(self, weight=None, **kwargs): + self.WEIGHTS = { + 'global': 1, + 'template': 2, + 'case': 3, + 'alert': 3, + 'case_artifact': 4, + 'observable': 4 + } + self.allowed_keys = set([ + 'Queue', + 'Status', + 'Owner', + 'Requestor', + 'Cc', + 'AdminCc', + 'Subject', + 'Text', + 'Priority', + 'InitialPriority', + 'FinalPriority', + 'TimeEstimated', + 'Starts', + 'Due', + 'Files', + 'template', + 'indicator_list' + ]) + + # 'normal' dict init, no weight but requires key_to_list_mapping + if 'key_to_list_mapping' in kwargs: + super().__init__(kwargs.get('key_to_list_mapping')) + # RT4 init, be sure we have weights + else: + super().__init__(self) + self.__setitem__(weight, **kwargs) + + + # override default 'set' method so users can't accidentally set config items without a corresponding weight + def __setitem__(self, weight, **kwargs): + for key, value in kwargs.items(): + if key in self.allowed_keys or key.startswith('CF_'): + weight_key = "{}_weight".format(key) + # map string weight to int if needed + if isinstance(weight, str): + weight = self.WEIGHTS[weight] + if weight_key not in self or weight >= self[weight_key]: + # update weight key value with new weight + super().__setitem__(key, value) + super().__setitem__(weight_key, weight) + # if we're not an RT4 setting, don't worry about weights + # e.g., for case/artifact details we store in a config object + else: + super().__setitem__(key, value) + + # override default 'update' method to include weighting + def update(self, weight, **kwargs): + self.__setitem__(weight, **kwargs) + + # override default 'keys' method to only display keys related to RT4 + def keys(self): + for key in super().keys(): + if key in self.allowed_keys: + yield key + + # override default 'items' method to only iterate items related to RT4 + def items(self): + for key in super().keys(): + if key in self.allowed_keys: + yield key, self[key] + + # function to provide all items + def fullitems(self): + for key in super().keys(): + yield key, self[key] + + # create custom '__copy__' method. we do this so that copies don't include all the case/artifact details + def __copy__(self): + return self.__class__(**{'key_to_list_mapping': self.items()}) + + def copy(self): + "Returns a copy of this object." + return self.__copy__() \ No newline at end of file diff --git a/responders/RT4/requirements.txt b/responders/RT4/requirements.txt new file mode 100644 index 000000000..f47373772 --- /dev/null +++ b/responders/RT4/requirements.txt @@ -0,0 +1,4 @@ +defang +jinja2 +rt +requests \ No newline at end of file diff --git a/responders/RT4/rt4.json b/responders/RT4/rt4.json new file mode 100644 index 000000000..8da59fb99 --- /dev/null +++ b/responders/RT4/rt4.json @@ -0,0 +1,91 @@ +{ + "name": "RT4-CreateTicket", + "version": "1.0", + "author": "Michael Davis, REN-ISAC", + "url": "https://github.com/TheHive-Project/Cortex-Analyzers/tree/master/responders/RT4", + "license": "MIT", + "description": "Cortex Responder to create a ticket in RT4 from TheHive observables or alerts", + "dataTypeList": ["thehive:case_artifact", "thehive:alert", "thehive:case"], + "command": "RT4/rt4.py", + "baseConfig": "RT4", + "configurationItems": [ + { + "name": "server", + "description": "RT4 Base URL, e.g., https://rt.domain.local", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "username", + "description": "RT4 username for authentication", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "password", + "description": "RT4 password for user account", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "Queue", + "description": "Default queue in which to create new tickets", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "General" + }, + { + "name": "Owner", + "description": "Default owner to assign newly created tickets (optional)", + "type": "string", + "multi": false, + "required": false + }, + { + "name": "Status", + "description": "Default ticket status to assign newly created tickets (optional)", + "type": "string", + "multi": false, + "required": false + }, + { + "name": "custom_field_list", + "description": "Name:Value of Custom Fields in RT to set on every ticket created (e.g.: 'How Reported:TheHive' sets CF.{How Reported} = TheHive on every new ticket)", + "type": "string", + "multi": true, + "required": false + }, + { + "name": "tag_to_template_map", + "description": "Mapping table of tags to templates (e.g.: 'phishing:phish_letter' maps anything tagged as 'phishing' to the 'phish_letter' template)", + "type": "string", + "multi": true, + "required": true + }, + { + "name": "thehive_cf_rtticket", + "description": "Name of a case custom field in TheHive in which RT ticket #s will be saved upon successful case-level Responder run (optional)", + "type": "string", + "multi": false, + "required": false + }, + { + "name": "thehive_url", + "description": "TheHive Base URL, e.g., https://thehive.domain.local:9000 (optional: only needed to process Cases)", + "type": "string", + "multi": false, + "required": false + }, + { + "name": "thehive_token", + "description": "TheHive API token for authentication (optional: only needed to process Cases)", + "type": "string", + "multi": false, + "required": false + } + ] +} diff --git a/responders/RT4/rt4.py b/responders/RT4/rt4.py new file mode 100644 index 000000000..2fa01dfbb --- /dev/null +++ b/responders/RT4/rt4.py @@ -0,0 +1,387 @@ +#!/usr/bin/env python3 +# encoding: utf-8 + +from cortexutils.responder import Responder +from rt import Rt +from rt import ConnectionError +from template import NotificationContext +from config import RT4ResponderConfig +from datetime import datetime +from collections import defaultdict +from defang import defang +import json + +class RT4(Responder): + + def __init__(self): + Responder.__init__(self) + self.server = self.get_param('config.server', None, 'Missing RT4 server') + self.server = self.server.rstrip('/') + self.username = self.get_param('config.username', None, 'Missing RT4 username') + self.password = self.get_param('config.password', None, 'Missing RT4 password') + self.tag_to_template_map = self.get_param('config.tag_to_template_map') + self.thehive_cf_rtticket = self.get_param('config.thehive_cf_rtticket') + + cf_list_tmp = self.get_param('config.custom_field_list', None) + + if cf_list_tmp is not None: + cf_dict_tmp = {} + for cf_item in cf_list_tmp: + if cf_item is not None: + cf_name, cf_value = cf_item.split(':', 1) + cf_dict_tmp['CF_'+ cf_name] = cf_value + else: + cf_dict_tmp = None + + global_config = { + 'Queue': self.get_param('config.Queue', None, 'Missing default queue'), + 'Owner': self.get_param('config.Owner', None), + 'Status': self.get_param('config.Status', None), + 'template': self.get_param('config.template', None) + } + global_config.update(cf_dict_tmp) + + # init global config + self.config = RT4ResponderConfig(weight='global', **global_config) + + # create map for ticket creation arguments that will convert case(capitalization) + # to what's expected by rt module + self.TICKET_ARGS_MAP = { + 'cc': 'Cc', + 'admincc': 'AdminCc', + 'subject': 'Subject', + 'owner': 'Owner', + 'queue': 'Queue', + 'status': 'Status', + 'requestor': 'Requestor', + 'requestors': 'Requestor' + } + + def run(self): + Responder.run(self) + self.instance_type = self.get_param('data._type') + observable_list = [] + + # case observable details + if self.instance_type == 'case_artifact': + instance_data = self.get_param('data', None, 'Missing indicator') + # process case tags first + case_tags = self.get_param('data.case.tags') + case_config = self.process_tags(case_tags) + self.config.update(weight='case', **case_config) + + + # case details + if self.instance_type == 'case': + """ + api GET for case details don't include references to its observables + POST to thehive/api/case/artifact/_search with json body + { + "query": { "_parent": { "_type": case", "_query": { "_id": "<>" } } }, + "range": "all" + } + should return a list of dicts which are populated with k,v characteristic of artifacts. + """ + import requests + thehive_url = self.get_param('config.thehive_url', None, """ + Missing URL for TheHive. Must have configured this Responder setting to process Cases.""") + thehive_token = self.get_param('config.thehive_token', None, """ + Missing API token for TheHive. Must have configured this Responder setting to process Cases.""") + case_id = self.get_param('data._id') + + payload = { + "query": { "_parent": { "_type": "case", "_query": { "_id": case_id } } }, + "range": "all" + } + headers = { 'Content-Type': 'application/json', 'Authorization': 'Bearer {}'.format(thehive_token) } + thehive_api_url_case_search = '{}/api/case/artifact/_search'.format(thehive_url) + r = requests.post(thehive_api_url_case_search, data=json.dumps(payload), headers=headers) + + if r.status_code != requests.codes.ok: + self.error(json.dumps(r.text)) + + instance_data = r.json() + + # alert details + if self.instance_type == 'alert': + instance_data = self.get_param('data.artifacts', None, 'Missing artifacts') + + # process artifact/observable/case tags + obs_tags = self.get_param('data.tags') + config_from_tags = self.process_tags(obs_tags) + self.config.update(weight=self.instance_type, **config_from_tags) + # only ever have one observable for cases, but could have multiples for other types + observable_list.extend(self.process_observables(instance_data)) + # should iterate the observable_list and merge the indicator_lists of any observables that share + # non-differing configs + observable_list = self.dedupe_and_merge(observable_list) + + # for each ticket creation, log return info to return_info dict in either the 'failures' key is failed, + # or the 'successes' key (which is a nested dict with k,v where k = rt_ticket # and v = ticket settings) + self.return_info = defaultdict(list) + for observable in observable_list: + new_ticket, rt_ticket_submission = self.create_rt_ticket(observable) + if new_ticket == -1: + msg = """RT ticket creation error. Possibly bad data such as non-existent Owner or Queue; + or data that does not correspond to an RT field. Observable info: {}""".format(observable) + self.return_info['failures'].append(msg) + else: + msg = """Ticket #{} created in Request Tracker with these settings: + \n{}""".format(new_ticket, rt_ticket_submission) + ticket_url = self.server + '/Ticket/Display.html?id={}'.format(new_ticket) + self.return_info['successes'].append({ 'id': new_ticket, 'msg': msg, 'ticket_url': ticket_url }) + + if 'successes' not in self.return_info: + self.error(json.dumps(self.return_info)) + else: + self.report({'message': json.dumps(self.return_info)}) + + + def operations(self, raw): + # if we had any successfully created tickets, get the corresponding RT ticket nums to add to a hive custom field + # convert 'successes' dict keys (ticket ids) to a list of ints, then ints to strings to join them as csv + created_tickets = [] + for ticket in self.return_info['successes']: + created_tickets.append(ticket['ticket_url']) + created_tickets = ', '.join([str(i) for i in created_tickets]) + + + if self.instance_type == 'case_artifact': + return [self.build_operation('AddTagToArtifact', tag='rt4:submitted'), + self.build_operation('AddCustomFields', name=self.thehive_cf_rtticket, value=created_tickets, tpe='string')] + elif self.instance_type == 'alert': + return [self.build_operation('AddTagToAlert', tag='rt4:submitted'), + self.build_operation('AddCustomFields', name=self.thehive_cf_rtticket, value=created_tickets, tpe='string')] + elif self.instance_type == 'case': + return [self.build_operation('AddTagToCase', tag='rt4:submitted'), + self.build_operation('AddCustomFields', name=self.thehive_cf_rtticket, value=created_tickets, tpe='string')] + + def process_observables(self, data): + observable_list = [] + # if we were handed a single dict instead of a list, make it a list of 1 + if not isinstance(data, list): + data = [data] + for i in data: + # setup a config for each observable + obs_config_tmp = { + 'indicator_list': [i['data']] + } + obs_config_from_tags = self.process_tags(i['tags']) + # merge all hive data on input object w/ config from tags + tmp_dict = ({**self.get_param('data'), **obs_config_tmp}) + tmp_dict = ({**tmp_dict, **obs_config_from_tags}) + # tmp_dict = ({**obs_config_tmp, **obs_config_from_tags}) + # merged into a dict but needs to be converted to RT4ResponderConfig obj + tmp_dict = ({**self.config, **tmp_dict}) + observable = RT4ResponderConfig('observable', **tmp_dict) + observable_list.append(observable) + + return observable_list + + def dedupe_and_merge(self, observable_list): + """Takes a list of dict observables and removes any duplicates while merging observables where the + only difference is the indicator (implying that if all other config settings are the same, they can + be sent in the same RT4 ticket notification). + Input: list of RT4ResponderConfig objects + Output: list of deduped/merged RT4ResponderConfig objects + """ + deduped_list = [] + seen = set() + for item in observable_list: + h = item.__copy__() + # pop off indicator_list key so as not to compare that one since if it's diff, we can just merge it later + h.pop('indicator_list') + # convert dict to hashable type (tuple, in this case) for comparison + h = tuple(h.items()) + if h not in seen: + seen.add(h) + deduped_list.append(item) + else: + for obs in deduped_list: + # check item against all observables in observable_list to see if the only diff is 'indicator_list' + compare_result = self._dict_compare(item, obs)[2] + if len(compare_result) == 1 and 'indicator_list' in compare_result: + # if we get here, the obs were the same, so see if value of indicator_list key is unique + if item['indicator_list'] not in obs['indicator_list']: + obs['indicator_list'].extend(item['indicator_list']) + + return deduped_list + + def process_tags(self, tags): + processed_tags = {} + tmpl_tag = None + template = None + mail_tags = [] + cc_tags = defaultdict(list) + + for tag in tags: + # snag any tag used for setting rt4 ticket values and split into name and value + # (except requestor which is handled elsewhere) + if tag.lower().startswith('rt4_set_') and not tag.lower().startswith('rt4_set_requestor'): + rt_setting_name, rt_setting_value = tag.split('rt4_set_')[1].split(':', 1) + # handle custom fields if present since the format is slightly different than other args + if rt_setting_name.lower().startswith('cf_'): + cf_name = 'CF_' + rt_setting_name.split('cf_')[1] + cf_value = rt_setting_value + processed_tags.update({cf_name : cf_value}) + elif rt_setting_name.lower().startswith('template'): + tmpl_tag = rt_setting_value + # cover cc, bcc, or admincc tags + elif rt_setting_name.lower().endswith('cc'): + rt_setting_name = self.TICKET_ARGS_MAP[rt_setting_name] + + cc_tags[rt_setting_name].append(rt_setting_value) + else: + try: + rt_setting_name = self.TICKET_ARGS_MAP[rt_setting_name] + except KeyError as e: + self.error('One of the rt4_set_ tags was not recognized: {}'.format(e)) + processed_tags.update({rt_setting_name : rt_setting_value}) + + elif tag.lower().startswith('contact:') or tag.lower().startswith('rt4_set_requestor'): + mail_tags.append(tag.split(':', 1)[1]) + + # map tags to a template if: + # (1) overriding rt4_set_template NOT present and + # (2) appropriate match found + if not tmpl_tag: + for mapping in self.tag_to_template_map: + map_tag, map_template = mapping.split(':', 1) + if map_tag == tag: + template = map_template + # allow overriding of template_name if appropriate rt4_set_template tag was present + else: + template = tmpl_tag + + # convert list of contacts to comma-separated string + if mail_tags: + requestor_list = u', '.join(mail_tags) + processed_tags.update({'Requestor' : requestor_list}) + + # convert list of admincc/cc/bcc to comma-separated string and merge into processed_tags + """processed_tags should be a dict of all tags and values that were processed, e.g.: + { "Owner": "root", + "CF_Classification": "phishing_generic", + "Queue": "Incident Reports", + "Requestor": "staff1@dom.org, staff2@dom.org" + } + """ + if cc_tags: + """cc_tags should be a dict of all admincc/bcc/cc tags and values that were processed, e.g.: + { "AdminCc": "staff3@dom.org, outsider@anotherdom.org" } + """ + for key, val in cc_tags.items(): + cc_tags[key] = u', '.join(val) + + # merge cc_tags into processed_tags + processed_tags.update(cc_tags) + + # see if template var was ever defined above; if not, do nothing; if so, add to dict + if template is not None: + processed_tags.update({'template' : template}) + + return processed_tags + + def create_rt_ticket(self, observable): + # create an observable config item that will be used to contain all observable and template info + # to pass along to RT during ticket creation + obs_config = RT4ResponderConfig(weight='case', **self.config) + obs_config.update(weight='observable', **observable) + # defang indicators and write them back as a single string joined together by newlines + if 'indicator_list' in observable: + indicator_list = defang(u'\n'.join(observable['indicator_list'])) + observable.update(weight='observable', **{ 'indicator_list': indicator_list} ) + else: + self.error("""Unable to find indicators on case/alert/observable: + {}""".format(json.dumps(observable, indent=1))) + + if 'template' in observable: + obs_config.update(weight='observable', **{ 'template': observable['template'] }) + if 'template' not in obs_config: + self.error(""" + Couldn't map a tag to a notification type. + Observable/alert/case must be tagged with one 'rt4_set_template:' tag, + where is the name of a file (without .j2 ext) in /templates dir""") + # render the notification template to be passed on to the observable config item + rendered_template = NotificationContext().render_blocks_to_dict( + template_name=obs_config['template'], + kwargs=observable + ) + obs_config.update(weight='template', **rendered_template) + + if 'Requestor' in observable: + obs_config.update(weight='observable', **{ 'Requestor': observable['Requestor'] }) + if 'Requestor' not in obs_config: + self.error(""" + Case/alert/observable must be tagged with at least one 'contact:abuse@domain.local' or + set_rt4_requestor:abuse@domain.local tag with an appropriate email address""") + + # build session dict + rt_session = { + 'url': self.server + "/REST/1.0/", + 'default_login': self.username, + 'default_password': self.password + } + + # create ticket dict + rt_ticket = {} + + # add additional k,v pairs (as long as it's not the template or indicator_list since those are not accepted + # as params to the Rt py module for RT ticket creation) + for key, value in obs_config.items(): + if obs_config[key] is not None and key != 'indicator_list' and key != 'template': + rt_ticket[key] = value + + # create rt session + try: + rt_session = Rt(**rt_session) + login_ret = rt_session.login() + except ConnectionError as e: + self.error("{}".format(e)) + except Exception as e: + self.error("Error: {}".format(e)) + if login_ret != True: + self.error('Authentication/Connection error to RT') + + # create ticket + try: + new_ticket = rt_session.create_ticket(**rt_ticket) + except Exception as e: + rt_session.logout() + self.error("""RT ticket creation error: {} Possibly bad data such as non-existent Owner or Queue; + or data that does not correspond to an RT field. + \nSent the following RT request: {}""".format(e, json.dumps(rt_ticket, indent=2))) + + rt_session.logout() + return new_ticket, rt_ticket + + def _dict_compare(self, d1, d2): + """Feed this function two dictionaries and it can return if there are any differences + Courtesy of: https://stackoverflow.com/a/18860653 + """ + try: + d1_keys = set(d1.keys()) + d2_keys = set(d2.keys()) + except: + self.error("""Could not get keys from dicts for comparison. dict1: + {}\ndict2: {}""".format(json.dumps(d1), json.dumps(d2)) + ) + intersect_keys = d1_keys.intersection(d2_keys) + added = d1_keys - d2_keys + removed = d2_keys - d1_keys + modified = {o : [d1[o], d2[o]] for o in intersect_keys if d1[o] != d2[o]} + same = set(o for o in intersect_keys if d1[o] == d2[o]) + return added, removed, modified, same + +def _flatten(arr: list): + """ Flattens arbitrarily-nested list `arr` into single-dimensional. + Courtesy of: https://stackoverflow.com/a/54306091 + """ + while arr: + if isinstance(arr[0], list): # Checks whether first element is a list + arr = arr[0] + arr[1:] # If so, flattens that first element one level + else: + yield arr.pop(0) # Otherwise yield as part of the flat array + +if __name__ == '__main__': + RT4().run() diff --git a/responders/RT4/template.py b/responders/RT4/template.py new file mode 100644 index 000000000..192ec17d4 --- /dev/null +++ b/responders/RT4/template.py @@ -0,0 +1,32 @@ +import os +from jinja2 import Environment, FileSystemLoader + +class NotificationContext(): + def __init__(self, template_dir = 'templates'): + if os.path.isdir(template_dir): + self.template_dir = template_dir + else: + self.template_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), template_dir) + self.env = Environment(loader=FileSystemLoader(self.template_dir), trim_blocks=True) + + def render_blocks_to_dict(self, template_name='', kwargs=''): + """Given a template name and kwargs, returns all blocks w/ rendered text + Inputs: + - template_name (str): name of template, will be appended with .j2 + - kwargs (ptr): any keyword variable from the template file + Outputs: + - return_dict (dict): dictionary of k,v where keys are template block names and values are the rendered + text within each + Example: + rendered_dict = NotificationContext().render_blocks_to_dict(template_name=mabna,domain='bad.domain.ml') + """ + template_path = template_name + '.j2' + template = self.env.get_template(template_path) + return_dict = {} + template_ctx = template.new_context + + # render and return the jinja tmpl blocks as strings with leading/trailing whitespace stripped + for block_name, block_text in template.blocks.items(): + return_dict[block_name] = u''.join(block_text(template_ctx(vars=kwargs))).strip() + + return return_dict diff --git a/responders/RT4/templates/malware.j2 b/responders/RT4/templates/malware.j2 new file mode 100644 index 000000000..0d025036d --- /dev/null +++ b/responders/RT4/templates/malware.j2 @@ -0,0 +1,24 @@ +{% block CF_Classification %} +malware +{% endblock %} + +{% block Subject %} +** Alert ** Malware Detected from Your IP Range +{% endblock %} + + +{% block Text %} +We have detected malware coming from your IP space: + +Malware Type(s): +{{ description }} + +Let us know if we can help you look into it. + +On behalf of the Great SOC Team, + +-- +team@greatsoc.tld +24x7 Watch Desk +1(555)555-1212 +https://www.greatsoc.tld +{% endblock %} diff --git a/responders/RT4/templates/phishing_generic.j2 b/responders/RT4/templates/phishing_generic.j2 new file mode 100644 index 000000000..5621bce06 --- /dev/null +++ b/responders/RT4/templates/phishing_generic.j2 @@ -0,0 +1,24 @@ +{% block CF_Classification %} +phishing +{% endblock %} + +{% block Subject %} +** Alert ** Phishing Site Targeting Your Users +{% endblock %} + + +{% block Text %} +We have discovered the following potential phishing site(s) targeting your users: + +Domain(s): +{{ indicator_list }} + +We've noticed bad people trying to do bad things. Be on the lookout for nefarious d'er-do-wells from the above domain(s). + +On behalf of the Great SOC Team, + +-- +team@greatsoc.tld +24x7 Watch Desk +1(555)555-1234 +https://www.greatsoc.tld +{% endblock %} From bc6fa5978e39b8fba1cf488fdb12e981f40492dc Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Thu, 10 Oct 2019 08:39:00 -0500 Subject: [PATCH 2/2] update "Applies To" section --- responders/RT4/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/responders/RT4/README.md b/responders/RT4/README.md index 602640276..24d203653 100644 --- a/responders/RT4/README.md +++ b/responders/RT4/README.md @@ -1,7 +1,7 @@ # Request Tracker 4 Cortex Responder Summary: Creates RT tickets from TheHive -Applies To: Case Observables (Artifacts) +Applies To: Case Observables (Artifacts), Alerts, Cases ## Initial Responder Configuration