├── FireEye ├── __init__.py └── api.py ├── README.md ├── config.py.template ├── fe2th.py └── requirements.txt /FireEye/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LDO-CERT/FireEye2TH/edd84289be89588cfb95d3ac3caa477fd78c97e9/FireEye/__init__.py -------------------------------------------------------------------------------- /FireEye/api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import requests 5 | import json 6 | import hashlib 7 | import hmac 8 | import email 9 | from datetime import datetime, date, timedelta 10 | 11 | class FireEyeApi(): 12 | 13 | def __init__(self, config): 14 | 15 | """ 16 | Python API for FireEye 17 | :param config: FireEye configuration from config.py 18 | :type config: dict 19 | """ 20 | 21 | self.url = config['url'] 22 | self.public_key = config['fe_public_key'] 23 | self.private_key = config['fe_private_key'] 24 | self.proxies = config['proxies'] 25 | self.accept_version = '2.5' 26 | 27 | def exec_query(self, endpoint): 28 | time_stamp = email.utils.formatdate(localtime=True) 29 | accept_header = 'application/json' 30 | new_data = endpoint + self.accept_version + accept_header + time_stamp 31 | 32 | key = bytearray() 33 | key.extend(map(ord, self.private_key)) 34 | hashed = hmac.new(key, new_data.encode('utf-8'), hashlib.sha256) 35 | 36 | headers = { 37 | 'Accept': accept_header, 38 | 'Accept-Version': self.accept_version, 39 | 'X-Auth': self.public_key, 40 | 'X-Auth-Hash': hashed.hexdigest(), 41 | 'Date': time_stamp, 42 | } 43 | r = requests.get(self.url + endpoint, headers=headers, proxies=self.proxies) 44 | 45 | if r.status_code == 200: 46 | return r.status_code, r.json() 47 | else: 48 | return r.status_code, r.text 49 | 50 | def response(self, status, content): 51 | 52 | """ 53 | :param status: str = success/failure 54 | :type status: string 55 | :paran content: data to return 56 | :type content: dict 57 | :return: 58 | :rtype: dict 59 | 60 | """ 61 | 62 | return {'status':status, 'data': content} 63 | 64 | def get_incident(self, id): 65 | 66 | """ 67 | Fetch FireEye incident 68 | :param id: incident id 69 | :return: response 70 | :rtype: requests.get 71 | 72 | """ 73 | req = '/report/%s' % id 74 | status_code, data = self.exec_query(req) 75 | if status_code == 200: 76 | return self.response("success", data) 77 | else: 78 | return self.response("failure", data) 79 | 80 | def find_incidents(self, since): 81 | 82 | """ 83 | Fetch FireEye incidents since last `since` minutes 84 | :type since: int 85 | :rtype: request.post 86 | """ 87 | 88 | now = int((datetime.now() - timedelta(minutes=since)).timestamp()) 89 | 90 | req = '/report/index?since=%d' % now 91 | status_code, data = self.exec_query(req) 92 | if status_code == 200: 93 | return self.response("success", data) 94 | else: 95 | return self.response("failure", data) 96 | 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FireEye2TH: FireEye iSIGHT Alert Feeder for TheHive 2 | [FireEye](https://www.FireEye.com/) is a commercial Threat 3 | Intelligence provider which, according to their website: 4 | 5 | > FireEye iSIGHT Threat Intelligence is a proactive, forward-looking means of qualifying threats poised to disrupt your business based on the intents, tools and tactics of the attacker. Our high-fidelity, comprehensive intelligence delivers visibility beyond the typical attack lifecycle, adding context and priority to global threats before, during and after an attack. 6 | It helps mitigate risk, bolster incident response, and enhance your overall security ecosystem. Get the intel you need to predict attack and refocus your attention on what matters most to your business. 7 | 8 | FireEye2TH is a free, open source FireEye iSIGHT alert feeder for 9 | TheHive. You can use it to import 10 | FireEye *incidents* as alerts in TheHive, where they can be previewed and 11 | transformed into new cases using pre-defined incident response templates or 12 | added into existing ones. 13 | 14 | FireEyes2TH is written in Python 3 by LDO-CERT. 15 | 16 | ## Overview 17 | FireEye2TH is made of several parts: 18 | 19 | - `FireEye/api.py` : the main library to interact with the 20 | FireEye API and fetch *incidents*. 21 | - `config.py.template` : a configuration template which contains all the 22 | necessary information to connect to the APIs of FireEye iSIGHT and TheHive. 23 | All information is required. 24 | - `fe2th.py` : the main program. It gets FireEye iSIGHT *incidents* and creates alerts in TheHive with a description containing 25 | all relevant information, and observables if any. 26 | 27 | ## Prerequisites 28 | You'll need Python 3, the `requests` library and [TheHive4py](https://github.com/CERT-BDF/TheHive4py), 29 | a Python client for TheHive. 30 | 31 | [html2text](http://alir3z4.github.io/html2text/) library is used to convert html response in markdown. 32 | 33 | Clone the repository then copy the `config.py.template` file as `config.py` 34 | and fill in the blanks: proxies if applicable, API keys, URLs, accounts 35 | pertaining to your FireEye iSIGHT subscription and your instance of TheHive. 36 | 37 | **Note**: you need TheHive 2.13 or better and an account with the ability to create alerts. 38 | 39 | Then install the Python requirements: 40 | 41 | `$ pip3 install -r requirements.txt` 42 | 43 | ## Configuration parameters 44 | `ignored_tags` in `config.py` contains a list of tags that you wants to ignore. 45 | 46 | Some of the available tags are: `intendedEffect,affectedSystem,ttp,affectedIndustry,targetedInformation,targetGeography` 47 | 48 | ## Usage 49 | Once your configuration file `config.py` is ready, use the main program to 50 | fetch or find FireEye (FE) *incidents*: 51 | 52 | ``` 53 | ./fe2th.py -h 54 | usage: fe2th.py [-h] [-d] {inc,find} ... 55 | 56 | Get FE iSIGHT alerts and create alerts in TheHive 57 | 58 | positional arguments: 59 | {inc,find} subcommand help 60 | inc fetch incidents by ID 61 | find find incidents in time 62 | 63 | optional arguments: 64 | -h, --help show this help message and exit 65 | -d, --debug generate a log file and and active debug logging 66 | ``` 67 | 68 | The program comes with 2 commands: 69 | - `inc` to fetch *incidents* by their IDs 70 | - `find` to fetch *incidents* published during the last M minutes. 71 | 72 | If you need debbuging information, add the `d`switch and the program will 73 | create a file called `fe2th.log`. It will be created in the same folder as the 74 | main program. 75 | 76 | ### Retrieve incidents specified by their ID 77 | 78 | ``` 79 | ./fe2th.py inc -h 80 | usage: fe2th.py inc [-h] [-i ID [ID ...]] [-I ID [ID ...]] 81 | 82 | optional arguments: 83 | -h, --help show this help message and exit 84 | -i ID [ID ...], --incidents ID [ID ...] 85 | Get FE incidents by ID 86 | ``` 87 | 88 | - `./fe2th.py inc -i 1234567 2345678` : fetch incidents with IDs 1234567 and 2345678. 89 | 90 | ### Retrieve incidents published during the last `M` minutes 91 | 92 | ``` 93 | ./fe2th.py find -h 94 | usage: fe2th.py find [-h] -l M [-m] 95 | 96 | optional arguments: 97 | -h, --help show this help message and exit 98 | -l M, --last M Get all incidents published during the last [M] minutes 99 | -m, --monitor active monitoring 100 | ``` 101 | 102 | - `./fe2th.py find -l 20` retrieves incidents published during the last 20 minutes. 103 | - `m` is a switch that creates a `fe2th.status` file. This is useful if you want to add the program as a cron job and monitor it. 104 | 105 | ### Use Cases 106 | 107 | - Fetch incident #123456 108 | 109 | ``` 110 | $ ./fe2th.py inc -i 123456 111 | ``` 112 | 113 | - Add a cron job and check for new published incidents every 10 mins: 114 | 115 | ``` 116 | */10 * * * * /path/to/fe2th.py find -l 15 117 | ``` 118 | 119 | - Enable logging: 120 | 121 | ``` 122 | */10 * * * * /path/to/fe2th.py -d find -l 15 123 | ``` 124 | 125 | This will create a `fe2th.log` file in the folder of the main program. 126 | 127 | ### Monitoring 128 | 129 | - Monitor the feeder 130 | 131 | ``` 132 | */10 * * * * /path/to/fe2th.py find -l 15 -m 133 | ``` 134 | 135 | The monitoring switch makes the program "touch" a file named 136 | `fe2th.status` once it has successfully finished. To monitor it, just check 137 | the modification date of this file and compare it to the frequency used 138 | in your crontab entry. 139 | -------------------------------------------------------------------------------- /config.py.template: -------------------------------------------------------------------------------- 1 | FireEye = { 2 | 'proxies':{ 3 | 'http': '', 4 | 'https': '' 5 | }, 6 | 'url':'https://api.isightpartners.com', 7 | 'fe_public_key':'', 8 | 'fe_private_key':'', 9 | 'ignored_tags': 'targetGeographies,tag2' 10 | } 11 | 12 | TheHive = { 13 | 'proxies':{ 14 | 'http': '', 15 | 'https': '' 16 | }, 17 | 'url':'http://xxx.xxx.xxx.xxx:9000', 18 | 'key':'', 19 | 'template':'' 20 | 21 | } 22 | 23 | 24 | -------------------------------------------------------------------------------- /fe2th.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | import getopt 7 | import argparse 8 | import datetime 9 | from io import BytesIO 10 | import base64 11 | import logging 12 | import html2text 13 | 14 | from FireEye.api import FireEyeApi 15 | from thehive4py.api import TheHiveApi 16 | from thehive4py.models import Alert, AlertArtifact 17 | 18 | from config import FireEye, TheHive 19 | 20 | 21 | class monitoring(): 22 | 23 | def __init__(self, file): 24 | self.monitoring_file = file 25 | 26 | def touch(self): 27 | 28 | """ 29 | touch status file when successfully terminated 30 | """ 31 | if os.path.exists(self.monitoring_file): 32 | os.remove(self.monitoring_file) 33 | open(self.monitoring_file, 'a').close() 34 | 35 | def add_tags(tags, content): 36 | 37 | """ 38 | add tag to tags 39 | 40 | :param tags: existing tags 41 | :type tags: list 42 | :param content: string, mainly like taxonomy 43 | :type content: string 44 | """ 45 | 46 | t = tags 47 | for newtag in content: 48 | t.append("FE:{}".format(newtag)) 49 | return t 50 | 51 | def th_alert_tags(incident, ignored_tags=None): 52 | 53 | """ 54 | Convert FE incident tags into TH tags 55 | 56 | :param incident: FE incident 57 | :type incident: dict 58 | :return: TH tags 59 | :rtype: list 60 | """ 61 | 62 | tags = [] 63 | add_tags(tags, ["id={}".format(incident.get('reportId')), "type={}".format(incident.get('type'))]) 64 | tag_section = incident.get('tagSection', {}).get('main', {}) 65 | for section in tag_section.keys(): 66 | for k, v in tag_section[section].items(): 67 | for sv in v: 68 | if ignored_tags and section in [x.strip() for x in ignored_tags.split(',')]: 69 | continue 70 | if type(sv) == str: 71 | add_tags(tags, ["{}={}".format(k, sv)]) 72 | elif type(v) == dict: 73 | add_tags(tags, ["{}={}".format(k, sv.get('name', 'None') )]) 74 | return tags 75 | 76 | def description_to_markdown(content): 77 | 78 | """ 79 | Convert FE html tag into markdown 80 | 81 | :param content: json response 82 | :return: markdown description 83 | :rtype: string 84 | """ 85 | 86 | h = html2text.HTML2Text() 87 | h.ignore_tables = True 88 | return "{0} {1} {2} {3} {4}".format( 89 | "**Type:** {0}\n\n**Published:** {1}\n\n**Identifier:** {2}\n\n".format( 90 | content.get('intelligenceType', "None"), 91 | content.get('publishedDate',"None"), 92 | content.get('reportId',"None") 93 | ), 94 | "----\n\n#### Summary #### \n\n{}\n\n".format(h.handle(content.get('execSummary'))) if content.get('execSummary') else '', 95 | "----\n\n#### Analysis #### \n\n{}\n\n".format(h.handle(content.get('analysis'))) if content.get('analysis') else '', 96 | "----\n\n#### Overview #### \n\n{}\n\n".format(h.handle(content.get('overview'))) if content.get('overview') else '', 97 | "----\n\n#### Mitigation #### \n\n{}: {}\n\n".format(h.handle(content.get('mitigation')), content.get('mitigationDetails', '')) if content.get('mitigation') else '' 98 | ) 99 | 100 | def th_severity(sev): 101 | 102 | """ 103 | convert FireEye severity in TH severity 104 | 105 | :param sev: FE severity 106 | :type sev: string 107 | :return TH severity 108 | :rtype: int 109 | """ 110 | 111 | severities = { 112 | 'NONE': 1, 113 | 'LOW': 1, 114 | 'MEDIUM': 2, 115 | 'HIGH': 3, 116 | 'CRITICAL': 3 117 | } 118 | return severities[sev] 119 | 120 | def add_alert_artefact(artefacts, dataType, data, tags, tlp): 121 | 122 | """ 123 | :type artefacts: array 124 | :type dataType: string 125 | :type data: string 126 | :type tags: array 127 | :type tlp: int 128 | :rtype: array 129 | """ 130 | 131 | return artefacts.append(AlertArtifact(tags=tags, 132 | dataType=dataType, 133 | data=data, 134 | message="From FireEye", 135 | tlp=tlp) 136 | ) 137 | 138 | def build_observables(observables): 139 | 140 | """ 141 | Convert FE observables into TheHive observables 142 | 143 | :param observables: observables from FE 144 | :type observables: dict 145 | :return: AlertArtifact 146 | :rtype: thehive4py.models AlertArtifact 147 | """ 148 | 149 | artefacts = [] 150 | if len(observables) > 0: 151 | 152 | for ioc in observables: 153 | a = AlertArtifact( 154 | data=ioc[0], 155 | dataType=ioc[1], 156 | message="Observable from FireEye.", 157 | tlp=2, 158 | tags=["src:FireEye"] 159 | ) 160 | artefacts.append(a) 161 | 162 | return artefacts 163 | 164 | def build_alert(incident, observables, ignored_tags=None): 165 | 166 | """ 167 | Convert FireEye alert into a TheHive Alert 168 | 169 | :param incident: Incident from FE 170 | :type incident: dict 171 | :param observables: observables from FE 172 | :type observables: dict 173 | :return: Thehive alert 174 | :rtype: thehive4py.models Alerts 175 | """ 176 | 177 | a = Alert(title="{}".format(incident.get('title')), 178 | tlp=2, 179 | severity=th_severity(incident.get('riskRating', 'NONE')), 180 | description=description_to_markdown(incident), 181 | type=incident.get('intelligenceType', None), 182 | tags=th_alert_tags(incident, ignored_tags), 183 | caseTemplate=TheHive['template'], 184 | source="FireEye", 185 | sourceRef=incident.get('reportId'), 186 | artifacts=build_observables(observables) 187 | ) 188 | logging.debug("build_alert: alert built for FE id #{}".format(incident.get('reportId'))) 189 | return a 190 | 191 | def find_incidents(feapi, since, ignored_tags=None): 192 | 193 | """ 194 | :param feapi: FireEye.api.FireEyeApi 195 | :param since: number of minutes 196 | :type since: int 197 | :return: list of thehive4py.models Alerts 198 | :rtype: array 199 | """ 200 | 201 | response = feapi.find_incidents(since) 202 | 203 | if response.get('status') == "success": 204 | all_data = response.get('data').get('message', []) 205 | report_ids = [x['reportId'] for x in all_data] 206 | logging.debug('find_incidents(): {} FE incident(s) downloaded'.format( len(report_ids) )) 207 | 208 | for report_id in report_ids: 209 | report_response = feapi.get_incident(report_id) 210 | report_data = report_response.get('data').get('message', {}).get('report', {}) 211 | logging.debug('find_incidents(): {} FE incident downloaded'.format( report_id )) 212 | observables = get_observables(report_data) 213 | yield build_alert(report_data, observables, ignored_tags) 214 | else: 215 | logging.debug("find_incidents(): Error while fetching incident #{}: {}".format(id, response.get('data'))) 216 | sys.exit("find_incidents(): Error while fetching incident #{}: {}".format(id, response.get('data'))) 217 | 218 | def get_incidents(feapi, id_list, ignored_tags=None): 219 | 220 | """ 221 | :type feapi: FireEye.api.FireEyeApi 222 | :param id_list: list of incident id 223 | :type id_list: array 224 | :return: TheHive alert 225 | :rtype: thehive4py.models Alert 226 | """ 227 | 228 | while id_list: 229 | id = id_list.pop() 230 | response = feapi.get_incident(id) 231 | if response.get('status') == 'success': 232 | data = response.get('data').get('message', {}).get('report', {}) 233 | logging.debug('get_incidents(): {} FE incident downloaded'.format( id )) 234 | observables = get_observables(data) 235 | yield build_alert(data, observables, ignored_tags) 236 | else: 237 | logging.debug("get_incidents(): Error while fetching incident #{}: {}".format(id, response.get('data'))) 238 | sys.exit("get_incidents: Error while fetching incident #{}: {}".format(id, response.get('data'))) 239 | 240 | def get_observables(data): 241 | 242 | """ 243 | :type data: json 244 | :param data: json report 245 | :return: list of tuple with description and type of observables 246 | :rtype: list 247 | """ 248 | 249 | observables = [] 250 | observables_network = data.get('tagSection', {}).get('networks', {}).get('network', []) 251 | observables_files = data.get('tagSection', {}).get('files', {}).get('file', []) 252 | observables_email = data.get('tagSection', {}).get('emails', {}).get('email', []) 253 | 254 | for obj in observables_network: 255 | if obj.get('ip'): 256 | observables.append( (obj.get('ip'), 'ip') ) 257 | if obj.get('domain'): 258 | observables.append( (obj.get('domain'), 'domain') ) 259 | if obj.get('url'): 260 | observables.append( (obj.get('url'), 'url') ) 261 | 262 | for obj in observables_files: 263 | if obj.get('sha256'): 264 | observables.append( (obj.get('sha256'), 'hash') ) 265 | 266 | for obj in observables_email: 267 | if obj.get('senderAddress'): 268 | observables.append( (obj.get('senderAddress'), 'email') ) 269 | 270 | return observables 271 | 272 | def create_thehive_alerts(config, alerts): 273 | 274 | """ 275 | :param config: TheHive config 276 | :type config: dict 277 | :param alerts: List of alerts 278 | :type alerts: list 279 | :return: create TH alert 280 | """ 281 | 282 | thapi = TheHiveApi(config.get('url', None), config.get('key'), config.get('password', None), 283 | config.get('proxies')) 284 | for a in alerts: 285 | thapi.create_alert(a) 286 | 287 | def run(): 288 | 289 | """ 290 | Download FireEye incident and create a new alert in TheHive 291 | """ 292 | 293 | def find(args, ignored_tags=None): 294 | if 'last' in args and args.last is not None: 295 | last = args.last.pop() 296 | 297 | incidents = find_incidents(feapi, last, ignored_tags) 298 | create_thehive_alerts(TheHive, incidents) 299 | 300 | if args.monitor: 301 | mon = monitoring("{}/fe2th.status".format( 302 | os.path.dirname(os.path.realpath(__file__)))) 303 | mon.touch() 304 | 305 | def inc(args, ignored_tags=None): 306 | if 'incidents' in args and args.incidents is not None: 307 | incidents = get_incidents(feapi, args.incidents, ignored_tags) 308 | create_thehive_alerts(TheHive, incidents) 309 | 310 | parser = argparse.ArgumentParser(description="Get FE incidents and create alerts in TheHive") 311 | parser.add_argument("-d", "--debug", 312 | action='store_true', 313 | default=False, 314 | help="generate a log file and active debug logging") 315 | subparsers = parser.add_subparsers(help="subcommand help") 316 | 317 | parser_incident = subparsers.add_parser('inc', help="fetch incidents by ID") 318 | parser_incident.add_argument("-i", "--incidents", 319 | metavar="ID", 320 | action='store', 321 | type=str, 322 | nargs='+', 323 | help="Get FE incidents by ID") 324 | parser_incident.set_defaults(func=inc) 325 | 326 | parser_find = subparsers.add_parser('find', 327 | help="find incidents in time") 328 | parser_find.add_argument("-l", "--last", 329 | metavar="M", 330 | nargs=1, 331 | type=int,required=True, 332 | help="Get all incidents published during\ 333 | the last [M] minutes") 334 | parser_find.add_argument("-m", "--monitor", 335 | action='store_true', 336 | default=False, 337 | help="active monitoring") 338 | parser_find.set_defaults(func=find) 339 | 340 | if len(sys.argv[1:]) == 0: 341 | parser.print_help() 342 | parser.exit() 343 | args = parser.parse_args() 344 | 345 | if args.debug: 346 | logging.basicConfig(filename='{}/fe2th.log'.format( 347 | os.path.dirname(os.path.realpath(__file__))), 348 | level='DEBUG', 349 | format='%(asctime)s %(levelname)s %(message)s') 350 | feapi = FireEyeApi(FireEye) 351 | args.func(args, FireEye['ignored_tags']) 352 | 353 | if __name__ == '__main__': 354 | run() 355 | 356 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | 3 | thehive4py 4 | 5 | html2text --------------------------------------------------------------------------------