From 63ba7580d3d45cdc93495ce04286f906ed294cd8 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Fri, 27 Jul 2018 23:13:47 +0200 Subject: [PATCH 001/207] chg: Updated csvimport to support files from csv export + import MISP objects --- misp_modules/modules/import_mod/csvimport.py | 105 ++++++++++++++++--- 1 file changed, 88 insertions(+), 17 deletions(-) diff --git a/misp_modules/modules/import_mod/csvimport.py b/misp_modules/modules/import_mod/csvimport.py index 5ccf287..9b19fc2 100644 --- a/misp_modules/modules/import_mod/csvimport.py +++ b/misp_modules/modules/import_mod/csvimport.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import json, os, base64 -import pymisp +from pymisp import __path__ as pymisp_path +from collections import defaultdict misperrors = {'error': 'Error'} moduleinfo = {'version': '0.1', 'author': 'Christian Studer', @@ -13,20 +14,49 @@ userConfig = {'header': { 'message': 'Define the header of the csv file, with types (included in MISP attribute types or attribute fields) separated by commas.\nFor fields that do not match these types, please use space or simply nothing between commas.\nFor instance: ip-src,domain, ,timestamp'}, 'has_header':{ 'type': 'Boolean', - 'message': 'Tick this box ONLY if there is a header line, NOT COMMENTED, in the file (which will be skipped atm).' + 'message': 'Tick this box ONLY if there is a header line, NOT COMMENTED, in the file.' }} duplicatedFields = {'mispType': {'mispComment': 'comment'}, 'attrField': {'attrComment': 'comment'}} attributesFields = ['type', 'value', 'category', 'to_ids', 'comment', 'distribution'] +misp_standard_csv_header = ['uuid','event_id','category','type','value','comment','to_ids','date', + 'object_relation','object_uuid','object_name','object_meta_category'] delimiters = [',', ';', '|', '/', '\t', ' '] class CsvParser(): - def __init__(self, header, has_header): - self.header = header - self.fields_number = len(header) - self.has_header = has_header - self.attributes = [] + def __init__(self, header, has_header, data): + if data[0].split(',') == misp_standard_csv_header: + self.header = misp_standard_csv_header + self.from_misp = True + self.data = data[1:] + else: + self.from_misp = False + self.has_header = has_header + if header: + self.header = header + self.fields_number = len(header) + self.parse_data(data) + else: + self.has_delimiter = True + self.fields_number, self.delimiter, self.header = self.get_delimiter_from_header(data[0]) + self.data = data + self.result = [] + + def get_delimiter_from_header(self, data): + delimiters_count = {} + for d in delimiters: + length = data.count(d) + if length > 0: + delimiters_count[d] = data.count(d) + if len(delimiters_count) == 0: + length = 0 + delimiter = None + header = [data] + else: + length, delimiter = max((n, v) for v, n in delimiters_count.items()) + header = data.split(delimiter) + return length + 1, delimiter, header def parse_data(self, data): return_data = [] @@ -45,6 +75,7 @@ class CsvParser(): return_data.append(l) # find which delimiter is used self.delimiter = self.find_delimiter() + if self.fields_number == 0: self.header = return_data[0].split(self.delimiter) self.data = return_data[1:] if self.has_header else return_data def parse_delimiter(self, line): @@ -56,6 +87,43 @@ class CsvParser(): _, delimiter = max((n, v) for v, n in self.delimiter_count.items()) return delimiter + def parse_csv(self): + if self.from_misp: + self.build_misp_event() + else: + self.buildAttributes() + + def build_misp_event(self): + l_attributes = [] + l_objects = [] + objects = defaultdict(list) + attribute_fields = self.header[:1] + self.header[2:8] + relation_type = self.header[8] + object_fields = self.header[9:] + for line in self.data: + attribute = {} + try: + a_uuid,_,category,a_type,value,comment,to_ids,date,relation,o_uuid,o_name,o_meta_category = line.split(',') + except ValueError: + continue + for t, v in zip(attribute_fields, [a_uuid,category,a_type,value,comment,to_ids,date]): + attribute[t] = v.replace('"', '') + attribute['to_ids'] = True if to_ids == '1' else False + relation = relation.replace('"', '') + if relation: + attribute[relation_type] = relation + object_index = tuple(o.replace('"', '') for o in (o_uuid,o_name,o_meta_category)) + objects[object_index].append(attribute) + else: + l_attributes.append(attribute) + for keys, attributes in objects.items(): + misp_object = {} + for t, v in zip(['uuid','name','meta-category'], keys): + misp_object[t] = v + misp_object['Attribute'] = attributes + l_objects.append(misp_object) + self.result = {"Attribute": l_attributes, "Object": l_objects} + def buildAttributes(self): # if there is only 1 field of data if self.delimiter is None: @@ -63,7 +131,7 @@ class CsvParser(): for data in self.data: d = data.strip() if d: - self.attributes.append({'types': mispType, 'values': d}) + self.result.append({'types': mispType, 'values': d}) else: # split fields that should be recognized as misp attribute types from the others list2pop, misp, head = self.findMispTypes() @@ -83,10 +151,10 @@ class CsvParser(): for h, ds in zip(head, datasplit): if h: attribute[h] = ds.strip() - self.attributes.append(attribute) + self.result.append(attribute) def findMispTypes(self): - descFilename = os.path.join(pymisp.__path__[0], 'data/describeTypes.json') + descFilename = os.path.join(pymisp_path[0], 'data/describeTypes.json') with open(descFilename, 'r') as f: MispTypes = json.loads(f.read())['result'].get('types') list2pop = [] @@ -124,18 +192,21 @@ def handler(q=False): else: misperrors['error'] = "Unsupported attributes type" return misperrors - if not request.get('config') and not request['config'].get('header'): - misperrors['error'] = "Configuration error" - return misperrors - header = request['config'].get('header').split(',') - header = [c.strip() for c in header] has_header = request['config'].get('has_header') has_header = True if has_header == '1' else False + if not request.get('config') and not request['config'].get('header'): + if has_header: + header = [] + else: + misperrors['error'] = "Configuration error" + return misperrors + else: + header = request['config'].get('header').split(',') + header = [c.strip() for c in header] csv_parser = CsvParser(header, has_header) - csv_parser.parse_data(data.split('\n')) # build the attributes csv_parser.buildAttributes() - r = {'results': csv_parser.attributes} + r = {'results': csv_parser.result} return r def introspection(): From 92fbcaeff60d82168351e4dbb49133ba26226308 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Sat, 28 Jul 2018 00:07:02 +0200 Subject: [PATCH 002/207] fix: Fixed changes omissions in handler function --- misp_modules/modules/import_mod/csvimport.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/misp_modules/modules/import_mod/csvimport.py b/misp_modules/modules/import_mod/csvimport.py index 9b19fc2..d7be52a 100644 --- a/misp_modules/modules/import_mod/csvimport.py +++ b/misp_modules/modules/import_mod/csvimport.py @@ -203,9 +203,9 @@ def handler(q=False): else: header = request['config'].get('header').split(',') header = [c.strip() for c in header] - csv_parser = CsvParser(header, has_header) + csv_parser = CsvParser(header, has_header, data.split('\n')) # build the attributes - csv_parser.buildAttributes() + csv_parser.parse_csv() r = {'results': csv_parser.result} return r From 7980aa045abaf4053bf2ad754eac7038c46edfd0 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Wed, 1 Aug 2018 17:59:00 +0200 Subject: [PATCH 003/207] fix: Handling the case of Context included in the csv file exported from MISP --- misp_modules/modules/import_mod/csvimport.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/misp_modules/modules/import_mod/csvimport.py b/misp_modules/modules/import_mod/csvimport.py index d7be52a..90505b2 100644 --- a/misp_modules/modules/import_mod/csvimport.py +++ b/misp_modules/modules/import_mod/csvimport.py @@ -22,11 +22,14 @@ duplicatedFields = {'mispType': {'mispComment': 'comment'}, attributesFields = ['type', 'value', 'category', 'to_ids', 'comment', 'distribution'] misp_standard_csv_header = ['uuid','event_id','category','type','value','comment','to_ids','date', 'object_relation','object_uuid','object_name','object_meta_category'] +misp_context_additional_fields = ['event_info','event_member_org','event_source_org','event_distribution', + 'event_threat_level_id','event_analysis','event_date','event_tag'] delimiters = [',', ';', '|', '/', '\t', ' '] class CsvParser(): def __init__(self, header, has_header, data): - if data[0].split(',') == misp_standard_csv_header: + data_header = data[0].split(',') + if data_header == misp_standard_csv_header or data_header == (misp_standard_csv_header + misp_context_additional_fields): self.header = misp_standard_csv_header self.from_misp = True self.data = data[1:] @@ -100,10 +103,11 @@ class CsvParser(): attribute_fields = self.header[:1] + self.header[2:8] relation_type = self.header[8] object_fields = self.header[9:] + header_length = len(self.header) for line in self.data: attribute = {} try: - a_uuid,_,category,a_type,value,comment,to_ids,date,relation,o_uuid,o_name,o_meta_category = line.split(',') + a_uuid,_,category,a_type,value,comment,to_ids,date,relation,o_uuid,o_name,o_meta_category = line.split(',')[:header_length] except ValueError: continue for t, v in zip(attribute_fields, [a_uuid,category,a_type,value,comment,to_ids,date]): From 8b4d24ba635d424c38936dc37223ffe8c1adb779 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Thu, 2 Aug 2018 15:42:59 +0200 Subject: [PATCH 004/207] fix: Fixed fields parsing to support files from csv export with additional context --- misp_modules/modules/import_mod/csvimport.py | 29 +++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/misp_modules/modules/import_mod/csvimport.py b/misp_modules/modules/import_mod/csvimport.py index 90505b2..5b083a9 100644 --- a/misp_modules/modules/import_mod/csvimport.py +++ b/misp_modules/modules/import_mod/csvimport.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -import json, os, base64 +import base64, csv, io, json, os from pymisp import __path__ as pymisp_path from collections import defaultdict @@ -24,13 +24,14 @@ misp_standard_csv_header = ['uuid','event_id','category','type','value','comment 'object_relation','object_uuid','object_name','object_meta_category'] misp_context_additional_fields = ['event_info','event_member_org','event_source_org','event_distribution', 'event_threat_level_id','event_analysis','event_date','event_tag'] +misp_extended_csv_header = misp_standard_csv_header[:9] + ['attribute_tag'] + misp_standard_csv_header[9:] + misp_context_additional_fields delimiters = [',', ';', '|', '/', '\t', ' '] class CsvParser(): def __init__(self, header, has_header, data): - data_header = data[0].split(',') - if data_header == misp_standard_csv_header or data_header == (misp_standard_csv_header + misp_context_additional_fields): - self.header = misp_standard_csv_header + data_header = data[0] + if data_header == misp_standard_csv_header or data_header == misp_extended_csv_header: + self.header = misp_standard_csv_header if data_header == misp_standard_csv_header else misp_extended_csv_header[:13] self.from_misp = True self.data = data[1:] else: @@ -100,23 +101,24 @@ class CsvParser(): l_attributes = [] l_objects = [] objects = defaultdict(list) - attribute_fields = self.header[:1] + self.header[2:8] - relation_type = self.header[8] - object_fields = self.header[9:] header_length = len(self.header) + attribute_fields = self.header[:1] + self.header[2:6] for line in self.data: attribute = {} try: - a_uuid,_,category,a_type,value,comment,to_ids,date,relation,o_uuid,o_name,o_meta_category = line.split(',')[:header_length] + try: + a_uuid,_,a_category,a_type,value,comment,to_ids,_,relation,o_uuid,o_name,o_category = line[:header_length] + except ValueError: + a_uuid,_,a_category,a_type,value,comment,to_ids,_,relation,tag,o_uuid,o_name,o_category = line[:header_length] + if tag: attribute['tags'] = tag except ValueError: continue - for t, v in zip(attribute_fields, [a_uuid,category,a_type,value,comment,to_ids,date]): + for t, v in zip(attribute_fields, [a_uuid, a_category, a_type, value, comment]): attribute[t] = v.replace('"', '') attribute['to_ids'] = True if to_ids == '1' else False - relation = relation.replace('"', '') if relation: - attribute[relation_type] = relation - object_index = tuple(o.replace('"', '') for o in (o_uuid,o_name,o_meta_category)) + attribute["object_relation"] = relation.replace('"', '') + object_index = tuple(o.replace('"', '') for o in (o_uuid,o_name,o_category)) objects[object_index].append(attribute) else: l_attributes.append(attribute) @@ -193,6 +195,7 @@ def handler(q=False): request = json.loads(q) if request.get('data'): data = base64.b64decode(request['data']).decode('utf-8') + data = [line for line in csv.reader(io.TextIOWrapper(io.BytesIO(data.encode()), encoding='utf-8'))] else: misperrors['error'] = "Unsupported attributes type" return misperrors @@ -207,7 +210,7 @@ def handler(q=False): else: header = request['config'].get('header').split(',') header = [c.strip() for c in header] - csv_parser = CsvParser(header, has_header, data.split('\n')) + csv_parser = CsvParser(header, has_header, data) # build the attributes csv_parser.parse_csv() r = {'results': csv_parser.result} From d1000d82c4d14f50c68a606cec897849281bf2e0 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Tue, 5 Feb 2019 14:46:42 +0100 Subject: [PATCH 005/207] add: New module to check if a bitcoin address has been abused - Also related update of documentation --- README.md | 1 + doc/README.md | 1262 ++++++++++++++++- doc/documentation.md | 1243 ---------------- doc/expansion/btc_scam_check.json | 9 + doc/expansion/{btc.json => btc_steroids.json} | 0 doc/generate_documentation.py | 2 +- misp_modules/modules/expansion/__init__.py | 2 +- .../modules/expansion/btc_scam_check.py | 43 + 8 files changed, 1316 insertions(+), 1246 deletions(-) mode change 120000 => 100644 doc/README.md delete mode 100644 doc/documentation.md create mode 100644 doc/expansion/btc_scam_check.json rename doc/expansion/{btc.json => btc_steroids.json} (100%) create mode 100644 misp_modules/modules/expansion/btc_scam_check.py diff --git a/README.md b/README.md index 368ef6f..e8fa0d2 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ ### Expansion modules * [BGP Ranking](misp_modules/modules/expansion/bgpranking.py) - a hover and expansion module to expand an AS number with the ASN description, its history, and position in BGP Ranking. +* [BTC scam check](misp_modules/modules/expansion/btc_scam_check.py) - An expansion hover module to instantly check if a BTC address has been abused. * [BTC transactions](misp_modules/modules/expansion/btc_steroids.py) - An expansion hover module to get a blockchain balance and the transactions from a BTC address in MISP. * [CIRCL Passive DNS](misp_modules/modules/expansion/circl_passivedns.py) - a hover and expansion module to expand hostname and IP addresses with passive DNS information. * [CIRCL Passive SSL](misp_modules/modules/expansion/circl_passivessl.py) - a hover and expansion module to expand IP addresses with the X.509 certificate seen. diff --git a/doc/README.md b/doc/README.md deleted file mode 120000 index 0963ae8..0000000 --- a/doc/README.md +++ /dev/null @@ -1 +0,0 @@ -documentation.md \ No newline at end of file diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..e47470d --- /dev/null +++ b/doc/README.md @@ -0,0 +1,1261 @@ +# MISP modules documentation + +## Expansion Modules + +#### [bgpranking](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/bgpranking.py) + +Query BGP Ranking (https://bgpranking-ng.circl.lu/). +- **features**: +>The module takes an AS number attribute as input and displays its description and history, and position in BGP Ranking. +> +> +- **input**: +>Autonomous system number. +- **output**: +>Text containing a description of the ASN, its history, and the position in BGP Ranking. +- **references**: +>https://github.com/D4-project/BGP-Ranking/ +- **requirements**: +>pybgpranking python library + +----- + +#### [btc_scam_check](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/btc_scam_check.py) + + + +An expansion hover module to query a special dns blacklist to check if a bitcoin address has been abused. +- **features**: +>The module queries a dns blacklist directly with the bitcoin address and get a response if the address has been abused. +- **input**: +>btc address attribute. +- **output**: +>Text to indicate if the BTC address has been abused. +- **references**: +>https://btcblack.it/ +- **requirements**: +>dnspython3: dns python library + +----- + +#### [btc_steroids](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/btc_steroids.py) + + + +An expansion hover module to get a blockchain balance from a BTC address in MISP. +- **input**: +>btc address attribute. +- **output**: +>Text to describe the blockchain balance and the transactions related to the btc address in input. + +----- + +#### [circl_passivedns](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/circl_passivedns.py) + + + +Module to access CIRCL Passive DNS. +- **features**: +>This module takes a hostname, domain or ip-address (ip-src or ip-dst) attribute as input, and queries the CIRCL Passive DNS REST API to get and display information about this input. +> +>To make it work a username and a password are thus required to authenticate to the CIRCL Passive DNS API. +- **input**: +>Hostname, domain, or ip-address attribute. +- **ouput**: +>Text describing passive DNS information related to the input attribute. +- **references**: +>https://www.circl.lu/services/passive-dns/, https://datatracker.ietf.org/doc/draft-dulaunoy-dnsop-passive-dns-cof/ +- **requirements**: +>pypdns: Passive DNS python library, A CIRCL passive DNS account with username & password + +----- + +#### [circl_passivessl](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/circl_passivessl.py) + + + +Modules to access CIRCL Passive SSL. +- **features**: +>This module takes an ip-address (ip-src or ip-dst) attribute as input, and queries the CIRCL Passive SSL REST API to get and display information about this input. +> +>To make it work a username and a password are thus required to authenticate to the CIRCL Passive SSL API. +- **input**: +>Ip-address attribute. +- **output**: +>Text describing passive SSL information related to the input attribute. +- **references**: +>https://www.circl.lu/services/passive-ssl/ +- **requirements**: +>pypssl: Passive SSL python library, A CIRCL passive SSL account with username & password + +----- + +#### [countrycode](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/countrycode.py) + +Module to expand country codes. +- **features**: +>The module takes a domain or a hostname as input, and returns the country it belongs to. +> +>For non country domains, a list of the most common possible extensions is used. +- **input**: +>Hostname or domain attribute. +- **output**: +>Text with the country code the input belongs to. + +----- + +#### [crowdstrike_falcon](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/crowdstrike_falcon.py) + + + +Module to query Crowdstrike Falcon. +- **features**: +>This module takes a MISP attribute as input to query a CrowdStrike Falcon API. The API returns then the result of the query with some types we map into compatible types we add as MISP attributes. +> +>Please note that composite attributes composed by at least one of the input types mentionned below (domains, IPs, hostnames) are also supported. +- **input**: +>A MISP attribute included in the following list: +>- domain +>- email-attachment +>- email-dst +>- email-reply-to +>- email-src +>- email-subject +>- filename +>- hostname +>- ip-src +>- ip-dst +>- md5 +>- mutex +>- regkey +>- sha1 +>- sha256 +>- uri +>- url +>- user-agent +>- whois-registrant-email +>- x509-fingerprint-md5 +- **output**: +>MISP attributes mapped after the CrowdStrike API has been queried, included in the following list: +>- hostname +>- email-src +>- email-subject +>- filename +>- md5 +>- sha1 +>- sha256 +>- ip-dst +>- ip-dst +>- mutex +>- regkey +>- url +>- user-agent +>- x509-fingerprint-md5 +- **references**: +>https://www.crowdstrike.com/products/crowdstrike-falcon-faq/ +- **requirements**: +>A CrowdStrike API access (API id & key) + +----- + +#### [cve](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/cve.py) + + + +An expansion hover module to expand information about CVE id. +- **features**: +>The module takes a vulnerability attribute as input and queries the CIRCL CVE search API to get information about the vulnerability as it is described in the list of CVEs. +- **input**: +>Vulnerability attribute. +- **output**: +>Text giving information about the CVE related to the Vulnerability. +- **references**: +>https://cve.circl.lu/, https://cve.mitre.org/ + +----- + +#### [dbl_spamhaus](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/dbl_spamhaus.py) + + + +Module to check Spamhaus DBL for a domain name. +- **features**: +>This modules takes a domain or a hostname in input and queries the Domain Block List provided by Spamhaus to determine what kind of domain it is. +> +>DBL then returns a response code corresponding to a certain classification of the domain we display. If the queried domain is not in the list, it is also mentionned. +> +>Please note that composite MISP attributes containing domain or hostname are supported as well. +- **input**: +>Domain or hostname attribute. +- **output**: +>Information about the nature of the input. +- **references**: +>https://www.spamhaus.org/faq/section/Spamhaus%20DBL +- **requirements**: +>dnspython3: DNS python3 library + +----- + +#### [dns](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/dns.py) + +A simple DNS expansion service to resolve IP address from domain MISP attributes. +- **features**: +>The module takes a domain of hostname attribute as input, and tries to resolve it. If no error is encountered, the IP address that resolves the domain is returned, otherwise the origin of the error is displayed. +> +>The address of the DNS resolver to use is also configurable, but if no configuration is set, we use the Google public DNS address (8.8.8.8). +> +>Please note that composite MISP attributes containing domain or hostname are supported as well. +- **input**: +>Domain or hostname attribute. +- **output**: +>IP address resolving the input. +- **requirements**: +>dnspython3: DNS python3 library + +----- + +#### [domaintools](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/domaintools.py) + + + +DomainTools MISP expansion module. +- **features**: +>This module takes a MISP attribute as input to query the Domaintools API. The API returns then the result of the query with some types we map into compatible types we add as MISP attributes. +> +>Please note that composite attributes composed by at least one of the input types mentionned below (domains, IPs, hostnames) are also supported. +- **input**: +>A MISP attribute included in the following list: +>- domain +>- hostname +>- email-src +>- email-dst +>- target-email +>- whois-registrant-email +>- whois-registrant-name +>- whois-registrant-phone +>- ip-src +>- ip-dst +- **output**: +>MISP attributes mapped after the Domaintools API has been queried, included in the following list: +>- whois-registrant-email +>- whois-registrant-phone +>- whois-registrant-name +>- whois-registrar +>- whois-creation-date +>- text +>- domain +- **references**: +>https://www.domaintools.com/ +- **requirements**: +>Domaintools python library, A Domaintools API access (username & apikey) + +----- + +#### [eupi](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/eupi.py) + + + +A module to query the Phishing Initiative service (https://phishing-initiative.lu). +- **features**: +>This module takes a domain, hostname or url MISP attribute as input to query the Phishing Initiative API. The API returns then the result of the query with some information about the value queried. +> +>Please note that composite attributes containing domain or hostname are also supported. +- **input**: +>A domain, hostname or url MISP attribute. +- **output**: +>Text containing information about the input, resulting from the query on Phishing Initiative. +- **references**: +>https://phishing-initiative.eu/?lang=en +- **requirements**: +>pyeupi: eupi python library, An access to the Phishing Initiative API (apikey & url) + +----- + +#### [farsight_passivedns](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/farsight_passivedns.py) + + + +Module to access Farsight DNSDB Passive DNS. +- **features**: +>This module takes a domain, hostname or IP address MISP attribute as input to query the Farsight Passive DNS API. The API returns then the result of the query with some information about the value queried. +- **input**: +>A domain, hostname or IP address MISP attribute. +- **output**: +>Text containing information about the input, resulting from the query on the Farsight Passive DNS API. +- **references**: +>https://www.farsightsecurity.com/ +- **requirements**: +>An access to the Farsight Passive DNS API (apikey) + +----- + +#### [geoip_country](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/geoip_country.py) + + + +Module to query a local copy of Maxmind's Geolite database. +- **features**: +>This module takes an IP address MISP attribute as input and queries a local copy of the Maxmind's Geolite database to get information about the location of this IP address. +> +>Please note that composite attributes domain|ip are also supported. +- **input**: +>An IP address MISP Attribute. +- **output**: +>Text containing information about the location of the IP address. +- **references**: +>https://www.maxmind.com/en/home +- **requirements**: +>A local copy of Maxmind's Geolite database + +----- + +#### [hashdd](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/hashdd.py) + +A hover module to check hashes against hashdd.com including NSLR dataset. +- **features**: +>This module takes a hash attribute as input to check its known level, using the hashdd API. This information is then displayed. +- **input**: +>A hash MISP attribute (md5). +- **output**: +>Text describing the known level of the hash in the hashdd databases. +- **references**: +>https://hashdd.com/ + +----- + +#### [intelmq_eventdb](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/intelmq_eventdb.py) + + + +Module to access intelmqs eventdb. +- **features**: +>/!\ EXPERIMENTAL MODULE, some features may not work /!\ +> +>This module takes a domain, hostname, IP address or Autonomous system MISP attribute as input to query the IntelMQ database. The result of the query gives then additional information about the input. +- **input**: +>A hostname, domain, IP address or AS attribute. +- **output**: +>Text giving information about the input using IntelMQ database. +- **references**: +>https://github.com/certtools/intelmq, https://intelmq.readthedocs.io/en/latest/Developers-Guide/ +- **requirements**: +>psycopg2: Python library to support PostgreSQL, An access to the IntelMQ database (username, password, hostname and database reference) + +----- + +#### [ipasn](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/ipasn.py) + +Module to query an IP ASN history service (https://github.com/D4-project/IPASN-History). +- **features**: +>This module takes an IP address attribute as input and queries the CIRCL IPASN service to get additional information about the input. +- **input**: +>An IP address MISP attribute. +- **output**: +>Text describing additional information about the input after a query on the IPASN-history database. +- **references**: +>https://github.com/D4-project/IPASN-History +- **requirements**: +>pyipasnhistory: Python library to access IPASN-history instance + +----- + +#### [iprep](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/iprep.py) + +Module to query IPRep data for IP addresses. +- **features**: +>This module takes an IP address attribute as input and queries the database from packetmail.net to get some information about the reputation of the IP. +- **input**: +>An IP address MISP attribute. +- **output**: +>Text describing additional information about the input after a query on the IPRep API. +- **references**: +>https://github.com/mahesh557/packetmail +- **requirements**: +>An access to the packetmail API (apikey) + +----- + +#### [macaddress_io](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/macaddress_io.py) + + + +MISP hover module for macaddress.io +- **features**: +>This module takes a MAC address attribute as input and queries macaddress.io for additional information. +> +>This information contains data about: +>- MAC address details +>- Vendor details +>- Block details +- **input**: +>MAC address MISP attribute. +- **output**: +>Text containing information on the MAC address fetched from a query on macaddress.io. +- **references**: +>https://macaddress.io/, https://github.com/CodeLineFi/maclookup-python +- **requirements**: +>maclookup: macaddress.io python library, An access to the macaddress.io API (apikey) + +----- + +#### [onyphe](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/onyphe.py) + + + +Module to process a query on Onyphe. +- **features**: +>This module takes a domain, hostname, or IP address attribute as input in order to query the Onyphe API. Data fetched from the query is then parsed and MISP attributes are extracted. +- **input**: +>A domain, hostname or IP address MISP attribute. +- **output**: +>MISP attributes fetched from the Onyphe query. +- **references**: +>https://www.onyphe.io/, https://github.com/sebdraven/pyonyphe +- **requirements**: +>onyphe python library, An access to the Onyphe API (apikey) + +----- + +#### [onyphe_full](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/onyphe_full.py) + + + +Module to process a full query on Onyphe. +- **features**: +>This module takes a domain, hostname, or IP address attribute as input in order to query the Onyphe API. Data fetched from the query is then parsed and MISP attributes are extracted. +> +>The parsing is here more advanced than the one on onyphe module, and is returning more attributes, since more fields of the query result are watched and parsed. +- **input**: +>A domain, hostname or IP address MISP attribute. +- **output**: +>MISP attributes fetched from the Onyphe query. +- **references**: +>https://www.onyphe.io/, https://github.com/sebdraven/pyonyphe +- **requirements**: +>onyphe python library, An access to the Onyphe API (apikey) + +----- + +#### [otx](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/otx.py) + + + +Module to get information from AlienVault OTX. +- **features**: +>This module takes a MISP attribute as input to query the OTX Alienvault API. The API returns then the result of the query with some types we map into compatible types we add as MISP attributes. +- **input**: +>A MISP attribute included in the following list: +>- hostname +>- domain +>- ip-src +>- ip-dst +>- md5 +>- sha1 +>- sha256 +>- sha512 +- **output**: +>MISP attributes mapped from the result of the query on OTX, included in the following list: +>- domain +>- ip-src +>- ip-dst +>- text +>- md5 +>- sha1 +>- sha256 +>- sha512 +>- email +- **references**: +>https://www.alienvault.com/open-threat-exchange +- **requirements**: +>An access to the OTX API (apikey) + +----- + +#### [passivetotal](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/passivetotal.py) + + + + +- **features**: +>The PassiveTotal MISP expansion module brings the datasets derived from Internet scanning directly into your MISP instance. This module supports passive DNS, historic SSL, WHOIS, and host attributes. In order to use the module, you must have a valid PassiveTotal account username and API key. Registration is free and can be done by visiting https://www.passivetotal.org/register +- **input**: +>A MISP attribute included in the following list: +>- hostname +>- domain +>- ip-src +>- ip-dst +>- x509-fingerprint-sha1 +>- email-src +>- email-dst +>- target-email +>- whois-registrant-email +>- whois-registrant-phone +>- text +>- whois-registrant-name +>- whois-registrar +>- whois-creation-date +- **output**: +>MISP attributes mapped from the result of the query on PassiveTotal, included in the following list: +>- hostname +>- domain +>- ip-src +>- ip-dst +>- x509-fingerprint-sha1 +>- email-src +>- email-dst +>- target-email +>- whois-registrant-email +>- whois-registrant-phone +>- text +>- whois-registrant-name +>- whois-registrar +>- whois-creation-date +>- md5 +>- sha1 +>- sha256 +>- link +- **references**: +>https://www.passivetotal.org/register +- **requirements**: +>Passivetotal python library, An access to the PassiveTotal API (apikey) + +----- + +#### [rbl](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/rbl.py) + +Module to check an IPv4 address against known RBLs. +- **features**: +>This module takes an IP address attribute as input and queries multiple know Real-time Blackhost Lists to check if they have already seen this IP address. +> +>We display then all the information we get from those different sources. +- **input**: +>IP address attribute. +- **output**: +>Text with additional data from Real-time Blackhost Lists about the IP address. +- **references**: +>[RBLs list](https://github.com/MISP/misp-modules/blob/8817de476572a10a9c9d03258ec81ca70f3d926d/misp_modules/modules/expansion/rbl.py#L20) +- **requirements**: +>dnspython3: DNS python3 library + +----- + +#### [reversedns](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/reversedns.py) + +Simple Reverse DNS expansion service to resolve reverse DNS from MISP attributes. +- **features**: +>The module takes an IP address as input and tries to find the hostname this IP address is resolved into. +> +>The address of the DNS resolver to use is also configurable, but if no configuration is set, we use the Google public DNS address (8.8.8.8). +> +>Please note that composite MISP attributes containing IP addresses are supported as well. +- **input**: +>An IP address attribute. +- **output**: +>Hostname attribute the input is resolved into. +- **requirements**: +>DNS python library + +----- + +#### [securitytrails](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/securitytrails.py) + + + +An expansion modules for SecurityTrails. +- **features**: +>The module takes a domain, hostname or IP address attribute as input and queries the SecurityTrails API with it. +> +>Multiple parsing operations are then processed on the result of the query to extract a much information as possible. +> +>From this data extracted are then mapped MISP attributes. +- **input**: +>A domain, hostname or IP address attribute. +- **output**: +>MISP attributes resulting from the query on SecurityTrails API, included in the following list: +>- hostname +>- domain +>- ip-src +>- ip-dst +>- dns-soa-email +>- whois-registrant-email +>- whois-registrant-phone +>- whois-registrant-name +>- whois-registrar +>- whois-creation-date +>- domain +- **references**: +>https://securitytrails.com/ +- **requirements**: +>dnstrails python library, An access to the SecurityTrails API (apikey) + +----- + +#### [shodan](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/shodan.py) + + + +Module to query on Shodan. +- **features**: +>The module takes an IP address as input and queries the Shodan API to get some additional data about it. +- **input**: +>An IP address MISP attribute. +- **output**: +>Text with additional data about the input, resulting from the query on Shodan. +- **references**: +>https://www.shodan.io/ +- **requirements**: +>shodan python library, An access to the Shodan API (apikey) + +----- + +#### [sigma_queries](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/sigma_queries.py) + + + +An expansion hover module to display the result of sigma queries. +- **features**: +>This module takes a Sigma rule attribute as input and tries all the different queries available to convert it into different formats recognized by SIEMs. +- **input**: +>A Sigma attribute. +- **output**: +>Text displaying results of queries on the Sigma attribute. +- **references**: +>https://github.com/Neo23x0/sigma/wiki +- **requirements**: +>Sigma python library + +----- + +#### [sigma_syntax_validator](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/sigma_syntax_validator.py) + + + +An expansion hover module to perform a syntax check on sigma rules. +- **features**: +>This module takes a Sigma rule attribute as input and performs a syntax check on it. +> +>It displays then that the rule is valid if it is the case, and the error related to the rule otherwise. +- **input**: +>A Sigma attribute. +- **output**: +>Text describing the validity of the Sigma rule. +- **references**: +>https://github.com/Neo23x0/sigma/wiki +- **requirements**: +>Sigma python library, Yaml python library + +----- + +#### [sourcecache](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/sourcecache.py) + +Module to cache web pages of analysis reports, OSINT sources. The module returns a link of the cached page. +- **features**: +>This module takes a link or url attribute as input and caches the related web page. It returns then a link of the cached page. +- **input**: +>A link or url attribute. +- **output**: +>A malware-sample attribute describing the cached page. +- **references**: +>https://github.com/adulau/url_archiver +- **requirements**: +>urlarchiver: python library to fetch and archive URL on the file-system + +----- + +#### [stix2_pattern_syntax_validator](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/stix2_pattern_syntax_validator.py) + + + +An expansion hover module to perform a syntax check on stix2 patterns. +- **features**: +>This module takes a STIX2 pattern attribute as input and performs a syntax check on it. +> +>It displays then that the rule is valid if it is the case, and the error related to the rule otherwise. +- **input**: +>A STIX2 pattern attribute. +- **output**: +>Text describing the validity of the STIX2 pattern. +- **references**: +>[STIX2.0 patterning specifications](http://docs.oasis-open.org/cti/stix/v2.0/cs01/part5-stix-patterning/stix-v2.0-cs01-part5-stix-patterning.html) +- **requirements**: +>stix2patterns python library + +----- + +#### [threatcrowd](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/threatcrowd.py) + + + +Module to get information from ThreatCrowd. +- **features**: +>This module takes a MISP attribute as input and queries ThreatCrowd with it. +> +>The result of this query is then parsed and some data is mapped into MISP attributes in order to enrich the input attribute. +- **input**: +>A MISP attribute included in the following list: +>- hostname +>- domain +>- ip-src +>- ip-dst +>- md5 +>- sha1 +>- sha256 +>- sha512 +>- whois-registrant-email +- **output**: +>MISP attributes mapped from the result of the query on ThreatCrowd, included in the following list: +>- domain +>- ip-src +>- ip-dst +>- text +>- md5 +>- sha1 +>- sha256 +>- sha512 +>- hostname +>- whois-registrant-email +- **references**: +>https://www.threatcrowd.org/ + +----- + +#### [threatminer](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/threatminer.py) + + + +Module to get information from ThreatMiner. +- **features**: +>This module takes a MISP attribute as input and queries ThreatMiner with it. +> +>The result of this query is then parsed and some data is mapped into MISP attributes in order to enrich the input attribute. +- **input**: +>A MISP attribute included in the following list: +>- hostname +>- domain +>- ip-src +>- ip-dst +>- md5 +>- sha1 +>- sha256 +>- sha512 +- **output**: +>MISP attributes mapped from the result of the query on ThreatMiner, included in the following list: +>- domain +>- ip-src +>- ip-dst +>- text +>- md5 +>- sha1 +>- sha256 +>- sha512 +>- ssdeep +>- authentihash +>- filename +>- whois-registrant-email +>- url +>- link +- **references**: +>https://www.threatminer.org/ + +----- + +#### [urlscan](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/urlscan.py) + + + +An expansion module to query urlscan.io. +- **features**: +>This module takes a MISP attribute as input and queries urlscan.io with it. +> +>The result of this query is then parsed and some data is mapped into MISP attributes in order to enrich the input attribute. +- **input**: +>A domain, hostname or url attribute. +- **output**: +>MISP attributes mapped from the result of the query on urlscan.io. +- **references**: +>https://urlscan.io/ +- **requirements**: +>An access to the urlscan.io API + +----- + +#### [virustotal](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/virustotal.py) + + + +Module to get information from virustotal. +- **features**: +>This module takes a MISP attribute as input and queries the VirusTotal API with it, in order to get additional data on the input attribute. +> +>Multiple recursive requests on the API can then be processed on some attributes found in the first request. A limit can be set to restrict the number of values to query again, and at the same time the number of request submitted to the API. +> +>This limit is important because the default user VirusTotal apikey only allows to process a certain nunmber of queries per minute. As a consequence it is recommended to have a larger number of requests or a private apikey. +> +>Data is then mapped into MISP attributes. +- **input**: +>A domain, hash (md5, sha1, sha256 or sha512), hostname or IP address attribute. +- **output**: +>MISP attributes mapped from the rersult of the query on VirusTotal API. +- **references**: +>https://www.virustotal.com/ +- **requirements**: +>An access to the VirusTotal API (apikey) + +----- + +#### [vmray_submit](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/vmray_submit.py) + + + +Module to submit a sample to VMRay. +- **features**: +>This module takes an attachment or malware-sample attribute as input to query the VMRay API. +> +>The sample contained within the attribute in then enriched with data from VMRay mapped into MISP attributes. +- **input**: +>An attachment or malware-sample attribute. +- **output**: +>MISP attributes mapped from the result of the query on VMRay API, included in the following list: +>- text +>- sha1 +>- sha256 +>- md5 +>- link +- **references**: +>https://www.vmray.com/ +- **requirements**: +>An access to the VMRay API (apikey & url) + +----- + +#### [vulndb](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/vulndb.py) + + + +Module to query VulnDB (RiskBasedSecurity.com). +- **features**: +>This module takes a vulnerability attribute as input and queries VulnDB in order to get some additional data about it. +> +>The API gives the result of the query which can be displayed in the screen, and/or mapped into MISP attributes to add in the event. +- **input**: +>A vulnerability attribute. +- **output**: +>Additional data enriching the CVE input, fetched from VulnDB. +- **references**: +>https://vulndb.cyberriskanalytics.com/ +- **requirements**: +>An access to the VulnDB API (apikey, apisecret) + +----- + +#### [vulners](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/vulners.py) + + + +An expansion hover module to expand information about CVE id using Vulners API. +- **features**: +>This module takes a vulnerability attribute as input and queries the Vulners API in order to get some additional data about it. +> +>The API then returns details about the vulnerability. +- **input**: +>A vulnerability attribute. +- **output**: +>Text giving additional information about the CVE in input. +- **references**: +>https://vulners.com/ +- **requirements**: +>Vulners python library, An access to the Vulners API + +----- + +#### [whois](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/whois.py) + +Module to query a local instance of uwhois (https://github.com/rafiot/uwhoisd). +- **features**: +>This module takes a domain or IP address attribute as input and queries a 'Univseral Whois proxy server' to get the correct details of the Whois query on the input value (check the references for more details about this whois server). +- **input**: +>A domain or IP address attribute. +- **output**: +>Text describing the result of a whois request for the input value. +- **references**: +>https://github.com/rafiot/uwhoisd +- **requirements**: +>uwhois: A whois python library + +----- + +#### [wiki](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/wiki.py) + + + +An expansion hover module to extract information from Wikidata to have additional information about particular term for analysis. +- **features**: +>This module takes a text attribute as input and queries the Wikidata API. If the text attribute is clear enough to define a specific term, the API returns a wikidata link in response. +- **input**: +>Text attribute. +- **output**: +>Text attribute. +- **references**: +>https://www.wikidata.org +- **requirements**: +>SPARQLWrapper python library + +----- + +#### [xforceexchange](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/xforceexchange.py) + + + +An expansion module for IBM X-Force Exchange. +- **features**: +>This module takes a MISP attribute as input to query the X-Force API. The API returns then additional information known in their threats data, that is mapped into MISP attributes. +- **input**: +>A MISP attribute included in the following list: +>- ip-src +>- ip-dst +>- vulnerability +>- md5 +>- sha1 +>- sha256 +- **output**: +>MISP attributes mapped from the result of the query on X-Force Exchange. +- **references**: +>https://exchange.xforce.ibmcloud.com/ +- **requirements**: +>An access to the X-Force API (apikey) + +----- + +#### [yara_query](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/yara_query.py) + + + +An expansion & hover module to translate any hash attribute into a yara rule. +- **features**: +>The module takes a hash attribute (md5, sha1, sha256, imphash) as input, and is returning a YARA rule from it. This YARA rule is also validated using the same method as in 'yara_syntax_validator' module. +>Both hover and expansion functionalities are supported with this module, where the hover part is displaying the resulting YARA rule and the expansion part allows you to add the rule as a new attribute, as usual with expansion modules. +- **input**: +>MISP Hash attribute (md5, sha1, sha256, imphash, or any of the composite attribute with filename and one of the previous hash type). +- **output**: +>YARA rule. +- **references**: +>https://virustotal.github.io/yara/, https://github.com/virustotal/yara-python +- **requirements**: +>yara-python python library + +----- + +#### [yara_syntax_validator](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/yara_syntax_validator.py) + + + +An expansion hover module to perform a syntax check on if yara rules are valid or not. +- **features**: +>This modules simply takes a YARA rule as input, and checks its syntax. It returns then a confirmation if the syntax is valid, otherwise the syntax error is displayed. +- **input**: +>YARA rule attribute. +- **output**: +>Text to inform users if their rule is valid. +- **references**: +>http://virustotal.github.io/yara/ +- **requirements**: +>yara_python python library + +----- + +## Export Modules + +#### [cef_export](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/cef_export.py) + +Module to export a MISP event in CEF format. +- **features**: +>The module takes a MISP event in input, to look every attribute. Each attribute matching with some predefined types is then exported in Common Event Format. +>Thus, there is no particular feature concerning MISP Events since any event can be exported. However, 4 configuration parameters recognized by CEF format are required and should be provided by users before exporting data: the device vendor, product and version, as well as the default severity of data. +- **input**: +>MISP Event attributes +- **output**: +>Common Event Format file +- **references**: +>https://community.softwaregrp.com/t5/ArcSight-Connectors/ArcSight-Common-Event-Format-CEF-Guide/ta-p/1589306?attachment-id=65537 + +----- + +#### [goamlexport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/goamlexport.py) + + + +This module is used to export MISP events containing transaction objects into GoAML format. +- **features**: +>The module works as long as there is at least one transaction object in the Event. +> +>Then in order to have a valid GoAML document, please follow these guidelines: +>- For each transaction object, use either a bank-account, person, or legal-entity object to describe the origin of the transaction, and again one of them to describe the target of the transaction. +>- Create an object reference for both origin and target objects of the transaction. +>- A bank-account object needs a signatory, which is a person object, put as object reference of the bank-account. +>- A person can have an address, which is a geolocation object, put as object reference of the person. +> +>Supported relation types for object references that are recommended for each object are the folowing: +>- transaction: +> - 'from', 'from_my_client': Origin of the transaction - at least one of them is required. +> - 'to', 'to_my_client': Target of the transaction - at least one of them is required. +> - 'address': Location of the transaction - optional. +>- bank-account: +> - 'signatory': Signatory of a bank-account - the reference from bank-account to a signatory is required, but the relation-type is optional at the moment since this reference will always describe a signatory. +> - 'entity': Entity owning the bank account - optional. +>- person: +> - 'address': Address of a person - optional. +- **input**: +>MISP objects (transaction, bank-account, person, legal-entity, geolocation), with references, describing financial transactions and their origin and target. +- **output**: +>GoAML format file, describing financial transactions, with their origin and target (bank accounts, persons or entities). +- **references**: +>http://goaml.unodc.org/ +- **requirements**: +>PyMISP, MISP objects + +----- + +#### [liteexport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/liteexport.py) + +Lite export of a MISP event. +- **features**: +>This module is simply producing a json MISP event format file, but exporting only Attributes from the Event. Thus, MISP Events exported with this module should have attributes that are not internal references, otherwise the resulting event would be empty. +- **input**: +>MISP Event attributes +- **output**: +>Lite MISP Event + +----- + +#### [nexthinkexport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/nexthinkexport.py) + + + +Nexthink NXQL query export module +- **features**: +>This module export an event as Nexthink NXQL queries that can then be used in your own python3 tool or from wget/powershell +- **input**: +>MISP Event attributes +- **output**: +>Nexthink NXQL queries +- **references**: +>https://doc.nexthink.com/Documentation/Nexthink/latest/APIAndIntegrations/IntroducingtheWebAPIV2 + +----- + +#### [osqueryexport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/osqueryexport.py) + + + +OSQuery export of a MISP event. +- **features**: +>This module export an event as osquery queries that can be used in packs or in fleet management solution like Kolide. +- **input**: +>MISP Event attributes +- **output**: +>osquery SQL queries + +----- + +#### [pdfexport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/pdfexport.py) + +Simple export of a MISP event to PDF. +- **features**: +>The module takes care of the PDF file building, and work with any MISP Event. Except the requirement of asciidoctor, used to create the file, there is no special feature concerning the Event. +- **input**: +>MISP Event +- **output**: +>MISP Event in a PDF file. +- **references**: +>https://acrobat.adobe.com/us/en/acrobat/about-adobe-pdf.html +- **requirements**: +>PyMISP, asciidoctor + +----- + +#### [testexport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/testexport.py) + +Skeleton export module. + +----- + +#### [threatStream_misp_export](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/threatStream_misp_export.py) + + + +Module to export a structured CSV file for uploading to threatStream. +- **features**: +>The module takes a MISP event in input, to look every attribute. Each attribute matching with some predefined types is then exported in a CSV format recognized by ThreatStream. +- **input**: +>MISP Event attributes +- **output**: +>ThreatStream CSV format file +- **references**: +>https://www.anomali.com/platform/threatstream, https://github.com/threatstream +- **requirements**: +>csv + +----- + +#### [threat_connect_export](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/threat_connect_export.py) + + + +Module to export a structured CSV file for uploading to ThreatConnect. +- **features**: +>The module takes a MISP event in input, to look every attribute. Each attribute matching with some predefined types is then exported in a CSV format recognized by ThreatConnect. +>Users should then provide, as module configuration, the source of data they export, because it is required by the output format. +- **input**: +>MISP Event attributes +- **output**: +>ThreatConnect CSV format file +- **references**: +>https://www.threatconnect.com +- **requirements**: +>csv + +----- + +## Import Modules + +#### [csvimport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/csvimport.py) + +Module to import MISP attributes from a csv file. +- **features**: +>In order to parse data from a csv file, a header is required to let the module know which column is matching with known attribute fields / MISP types. +>This header is part of the configuration of the module and should be filled out in MISP plugin settings, each field separated by COMMAS. Fields that do not match with any type known in MISP can be ignored in import, using a space or simply nothing between two separators (example: 'ip-src, , comment, '). +>There is also one type that is confused and can be either a MISP attribute type or an attribute field: 'comment'. In this case, using 'attrComment' specifies that the attribute field 'comment' should be considered, otherwise it will be considered as the MISP attribute type. +> +>For each MISP attribute type, an attribute is created. +>Attribute fields that are imported are the following: value, type, category, to-ids, distribution, comment, tag. +- **input**: +>CSV format file. +- **output**: +>MISP Event attributes +- **references**: +>https://tools.ietf.org/html/rfc4180, https://tools.ietf.org/html/rfc7111 +- **requirements**: +>PyMISP + +----- + +#### [cuckooimport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/cuckooimport.py) + + + +Module to import Cuckoo JSON. +- **features**: +>The module simply imports MISP Attributes from a Cuckoo JSON format file. There is thus no special feature to make it work. +- **input**: +>Cuckoo JSON file +- **output**: +>MISP Event attributes +- **references**: +>https://cuckoosandbox.org/, https://github.com/cuckoosandbox/cuckoo + +----- + +#### [email_import](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/email_import.py) + +Module to import emails in MISP. +- **features**: +>This module can be used to import e-mail text as well as attachments and urls. +>3 configuration parameters are then used to unzip attachments, guess zip attachment passwords, and extract urls: set each one of them to True or False to process or not the respective corresponding actions. +- **input**: +>E-mail file +- **output**: +>MISP Event attributes + +----- + +#### [goamlimport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/goamlimport.py) + + + +Module to import MISP objects about financial transactions from GoAML files. +- **features**: +>Unlike the GoAML export module, there is here no special feature to import data from GoAML external files, since the module will import MISP Objects with their References on its own, as it is required for the export module to rebuild a valid GoAML document. +- **input**: +>GoAML format file, describing financial transactions, with their origin and target (bank accounts, persons or entities). +- **output**: +>MISP objects (transaction, bank-account, person, legal-entity, geolocation), with references, describing financial transactions and their origin and target. +- **references**: +>http://goaml.unodc.org/ +- **requirements**: +>PyMISP + +----- + +#### [mispjson](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/mispjson.py) + +Module to import MISP JSON format for merging MISP events. +- **features**: +>The module simply imports MISP Attributes from an other MISP Event in order to merge events together. There is thus no special feature to make it work. +- **input**: +>MISP Event +- **output**: +>MISP Event attributes + +----- + +#### [ocr](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/ocr.py) + +Optical Character Recognition (OCR) module for MISP. +- **features**: +>The module tries to recognize some text from an image and import the result as a freetext attribute, there is then no special feature asked to users to make it work. +- **input**: +>Image +- **output**: +>freetext MISP attribute + +----- + +#### [openiocimport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/openiocimport.py) + +Module to import OpenIOC packages. +- **features**: +>The module imports MISP Attributes from OpenIOC packages, there is then no special feature for users to make it work. +- **input**: +>OpenIOC packages +- **output**: +>MISP Event attributes +- **references**: +>https://www.fireeye.com/blog/threat-research/2013/10/openioc-basics.html +- **requirements**: +>PyMISP + +----- + +#### [threatanalyzer_import](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/threatanalyzer_import.py) + +Module to import ThreatAnalyzer archive.zip / analysis.json files. +- **features**: +>The module imports MISP Attributes from a ThreatAnalyzer format file. This file can be either ZIP, or JSON format. +>There is by the way no special feature for users to make the module work. +- **input**: +>ThreatAnalyzer format file +- **output**: +>MISP Event attributes +- **references**: +>https://www.threattrack.com/malware-analysis.aspx + +----- + +#### [vmray_import](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/vmray_import.py) + + + +Module to import VMRay (VTI) results. +- **features**: +>The module imports MISP Attributes from VMRay format, using the VMRay api. +>Users should then provide as the module configuration the API Key as well as the server url in order to fetch their data to import. +- **input**: +>VMRay format +- **output**: +>MISP Event attributes +- **references**: +>https://www.vmray.com/ +- **requirements**: +>vmray_rest_api + +----- diff --git a/doc/documentation.md b/doc/documentation.md deleted file mode 100644 index 31f09ed..0000000 --- a/doc/documentation.md +++ /dev/null @@ -1,1243 +0,0 @@ -# MISP modules documentation - -## Expansion Modules - -#### [bgpranking](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/bgpranking.py) - -Query BGP Ranking (https://bgpranking-ng.circl.lu/). -- **features**: ->The module takes an AS number attribute as input and displays its description and history, and position in BGP Ranking. -> -> -- **input**: ->Autonomous system number. -- **output**: ->Text containing a description of the ASN, its history, and the position in BGP Ranking. -- **references**: ->https://github.com/D4-project/BGP-Ranking/ -- **requirements**: ->pybgpranking python library - ------ - -#### [btc](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/btc.py) - - - -An expansion hover module to get a blockchain balance from a BTC address in MISP. -- **input**: ->btc address attribute. -- **output**: ->Text to describe the blockchain balance and the transactions related to the btc address in input. - ------ - -#### [circl_passivedns](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/circl_passivedns.py) - - - -Module to access CIRCL Passive DNS. -- **features**: ->This module takes a hostname, domain or ip-address (ip-src or ip-dst) attribute as input, and queries the CIRCL Passive DNS REST API to get and display information about this input. -> ->To make it work a username and a password are thus required to authenticate to the CIRCL Passive DNS API. -- **input**: ->Hostname, domain, or ip-address attribute. -- **ouput**: ->Text describing passive DNS information related to the input attribute. -- **references**: ->https://www.circl.lu/services/passive-dns/, https://datatracker.ietf.org/doc/draft-dulaunoy-dnsop-passive-dns-cof/ -- **requirements**: ->pypdns: Passive DNS python library, A CIRCL passive DNS account with username & password - ------ - -#### [circl_passivessl](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/circl_passivessl.py) - - - -Modules to access CIRCL Passive SSL. -- **features**: ->This module takes an ip-address (ip-src or ip-dst) attribute as input, and queries the CIRCL Passive SSL REST API to get and display information about this input. -> ->To make it work a username and a password are thus required to authenticate to the CIRCL Passive SSL API. -- **input**: ->Ip-address attribute. -- **output**: ->Text describing passive SSL information related to the input attribute. -- **references**: ->https://www.circl.lu/services/passive-ssl/ -- **requirements**: ->pypssl: Passive SSL python library, A CIRCL passive SSL account with username & password - ------ - -#### [countrycode](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/countrycode.py) - -Module to expand country codes. -- **features**: ->The module takes a domain or a hostname as input, and returns the country it belongs to. -> ->For non country domains, a list of the most common possible extensions is used. -- **input**: ->Hostname or domain attribute. -- **output**: ->Text with the country code the input belongs to. - ------ - -#### [crowdstrike_falcon](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/crowdstrike_falcon.py) - - - -Module to query Crowdstrike Falcon. -- **features**: ->This module takes a MISP attribute as input to query a CrowdStrike Falcon API. The API returns then the result of the query with some types we map into compatible types we add as MISP attributes. -> ->Please note that composite attributes composed by at least one of the input types mentionned below (domains, IPs, hostnames) are also supported. -- **input**: ->A MISP attribute included in the following list: ->- domain ->- email-attachment ->- email-dst ->- email-reply-to ->- email-src ->- email-subject ->- filename ->- hostname ->- ip-src ->- ip-dst ->- md5 ->- mutex ->- regkey ->- sha1 ->- sha256 ->- uri ->- url ->- user-agent ->- whois-registrant-email ->- x509-fingerprint-md5 -- **output**: ->MISP attributes mapped after the CrowdStrike API has been queried, included in the following list: ->- hostname ->- email-src ->- email-subject ->- filename ->- md5 ->- sha1 ->- sha256 ->- ip-dst ->- ip-dst ->- mutex ->- regkey ->- url ->- user-agent ->- x509-fingerprint-md5 -- **references**: ->https://www.crowdstrike.com/products/crowdstrike-falcon-faq/ -- **requirements**: ->A CrowdStrike API access (API id & key) - ------ - -#### [cve](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/cve.py) - - - -An expansion hover module to expand information about CVE id. -- **features**: ->The module takes a vulnerability attribute as input and queries the CIRCL CVE search API to get information about the vulnerability as it is described in the list of CVEs. -- **input**: ->Vulnerability attribute. -- **output**: ->Text giving information about the CVE related to the Vulnerability. -- **references**: ->https://cve.circl.lu/, https://cve.mitre.org/ - ------ - -#### [dbl_spamhaus](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/dbl_spamhaus.py) - - - -Module to check Spamhaus DBL for a domain name. -- **features**: ->This modules takes a domain or a hostname in input and queries the Domain Block List provided by Spamhaus to determine what kind of domain it is. -> ->DBL then returns a response code corresponding to a certain classification of the domain we display. If the queried domain is not in the list, it is also mentionned. -> ->Please note that composite MISP attributes containing domain or hostname are supported as well. -- **input**: ->Domain or hostname attribute. -- **output**: ->Information about the nature of the input. -- **references**: ->https://www.spamhaus.org/faq/section/Spamhaus%20DBL -- **requirements**: ->dnspython3: DNS python3 library - ------ - -#### [dns](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/dns.py) - -A simple DNS expansion service to resolve IP address from domain MISP attributes. -- **features**: ->The module takes a domain of hostname attribute as input, and tries to resolve it. If no error is encountered, the IP address that resolves the domain is returned, otherwise the origin of the error is displayed. -> ->The address of the DNS resolver to use is also configurable, but if no configuration is set, we use the Google public DNS address (8.8.8.8). -> ->Please note that composite MISP attributes containing domain or hostname are supported as well. -- **input**: ->Domain or hostname attribute. -- **output**: ->IP address resolving the input. -- **requirements**: ->dnspython3: DNS python3 library - ------ - -#### [domaintools](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/domaintools.py) - - - -DomainTools MISP expansion module. -- **features**: ->This module takes a MISP attribute as input to query the Domaintools API. The API returns then the result of the query with some types we map into compatible types we add as MISP attributes. -> ->Please note that composite attributes composed by at least one of the input types mentionned below (domains, IPs, hostnames) are also supported. -- **input**: ->A MISP attribute included in the following list: ->- domain ->- hostname ->- email-src ->- email-dst ->- target-email ->- whois-registrant-email ->- whois-registrant-name ->- whois-registrant-phone ->- ip-src ->- ip-dst -- **output**: ->MISP attributes mapped after the Domaintools API has been queried, included in the following list: ->- whois-registrant-email ->- whois-registrant-phone ->- whois-registrant-name ->- whois-registrar ->- whois-creation-date ->- text ->- domain -- **references**: ->https://www.domaintools.com/ -- **requirements**: ->Domaintools python library, A Domaintools API access (username & apikey) - ------ - -#### [eupi](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/eupi.py) - - - -A module to query the Phishing Initiative service (https://phishing-initiative.lu). -- **features**: ->This module takes a domain, hostname or url MISP attribute as input to query the Phishing Initiative API. The API returns then the result of the query with some information about the value queried. -> ->Please note that composite attributes containing domain or hostname are also supported. -- **input**: ->A domain, hostname or url MISP attribute. -- **output**: ->Text containing information about the input, resulting from the query on Phishing Initiative. -- **references**: ->https://phishing-initiative.eu/?lang=en -- **requirements**: ->pyeupi: eupi python library, An access to the Phishing Initiative API (apikey & url) - ------ - -#### [farsight_passivedns](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/farsight_passivedns.py) - - - -Module to access Farsight DNSDB Passive DNS. -- **features**: ->This module takes a domain, hostname or IP address MISP attribute as input to query the Farsight Passive DNS API. The API returns then the result of the query with some information about the value queried. -- **input**: ->A domain, hostname or IP address MISP attribute. -- **output**: ->Text containing information about the input, resulting from the query on the Farsight Passive DNS API. -- **references**: ->https://www.farsightsecurity.com/ -- **requirements**: ->An access to the Farsight Passive DNS API (apikey) - ------ - -#### [geoip_country](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/geoip_country.py) - - - -Module to query a local copy of Maxmind's Geolite database. -- **features**: ->This module takes an IP address MISP attribute as input and queries a local copy of the Maxmind's Geolite database to get information about the location of this IP address. -> ->Please note that composite attributes domain|ip are also supported. -- **input**: ->An IP address MISP Attribute. -- **output**: ->Text containing information about the location of the IP address. -- **references**: ->https://www.maxmind.com/en/home -- **requirements**: ->A local copy of Maxmind's Geolite database - ------ - -#### [hashdd](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/hashdd.py) - -A hover module to check hashes against hashdd.com including NSLR dataset. -- **features**: ->This module takes a hash attribute as input to check its known level, using the hashdd API. This information is then displayed. -- **input**: ->A hash MISP attribute (md5). -- **output**: ->Text describing the known level of the hash in the hashdd databases. -- **references**: ->https://hashdd.com/ - ------ - -#### [intelmq_eventdb](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/intelmq_eventdb.py) - - - -Module to access intelmqs eventdb. -- **features**: ->/!\ EXPERIMENTAL MODULE, some features may not work /!\ -> ->This module takes a domain, hostname, IP address or Autonomous system MISP attribute as input to query the IntelMQ database. The result of the query gives then additional information about the input. -- **input**: ->A hostname, domain, IP address or AS attribute. -- **output**: ->Text giving information about the input using IntelMQ database. -- **references**: ->https://github.com/certtools/intelmq, https://intelmq.readthedocs.io/en/latest/Developers-Guide/ -- **requirements**: ->psycopg2: Python library to support PostgreSQL, An access to the IntelMQ database (username, password, hostname and database reference) - ------ - -#### [ipasn](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/ipasn.py) - -Module to query an IP ASN history service (https://github.com/D4-project/IPASN-History). -- **features**: ->This module takes an IP address attribute as input and queries the CIRCL IPASN service to get additional information about the input. -- **input**: ->An IP address MISP attribute. -- **output**: ->Text describing additional information about the input after a query on the IPASN-history database. -- **references**: ->https://github.com/D4-project/IPASN-History -- **requirements**: ->pyipasnhistory: Python library to access IPASN-history instance - ------ - -#### [iprep](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/iprep.py) - -Module to query IPRep data for IP addresses. -- **features**: ->This module takes an IP address attribute as input and queries the database from packetmail.net to get some information about the reputation of the IP. -- **input**: ->An IP address MISP attribute. -- **output**: ->Text describing additional information about the input after a query on the IPRep API. -- **references**: ->https://github.com/mahesh557/packetmail -- **requirements**: ->An access to the packetmail API (apikey) - ------ - -#### [macaddress_io](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/macaddress_io.py) - - - -MISP hover module for macaddress.io -- **features**: ->This module takes a MAC address attribute as input and queries macaddress.io for additional information. -> ->This information contains data about: ->- MAC address details ->- Vendor details ->- Block details -- **input**: ->MAC address MISP attribute. -- **output**: ->Text containing information on the MAC address fetched from a query on macaddress.io. -- **references**: ->https://macaddress.io/, https://github.com/CodeLineFi/maclookup-python -- **requirements**: ->maclookup: macaddress.io python library, An access to the macaddress.io API (apikey) - ------ - -#### [onyphe](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/onyphe.py) - - - -Module to process a query on Onyphe. -- **features**: ->This module takes a domain, hostname, or IP address attribute as input in order to query the Onyphe API. Data fetched from the query is then parsed and MISP attributes are extracted. -- **input**: ->A domain, hostname or IP address MISP attribute. -- **output**: ->MISP attributes fetched from the Onyphe query. -- **references**: ->https://www.onyphe.io/, https://github.com/sebdraven/pyonyphe -- **requirements**: ->onyphe python library, An access to the Onyphe API (apikey) - ------ - -#### [onyphe_full](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/onyphe_full.py) - - - -Module to process a full query on Onyphe. -- **features**: ->This module takes a domain, hostname, or IP address attribute as input in order to query the Onyphe API. Data fetched from the query is then parsed and MISP attributes are extracted. -> ->The parsing is here more advanced than the one on onyphe module, and is returning more attributes, since more fields of the query result are watched and parsed. -- **input**: ->A domain, hostname or IP address MISP attribute. -- **output**: ->MISP attributes fetched from the Onyphe query. -- **references**: ->https://www.onyphe.io/, https://github.com/sebdraven/pyonyphe -- **requirements**: ->onyphe python library, An access to the Onyphe API (apikey) - ------ - -#### [otx](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/otx.py) - - - -Module to get information from AlienVault OTX. -- **features**: ->This module takes a MISP attribute as input to query the OTX Alienvault API. The API returns then the result of the query with some types we map into compatible types we add as MISP attributes. -- **input**: ->A MISP attribute included in the following list: ->- hostname ->- domain ->- ip-src ->- ip-dst ->- md5 ->- sha1 ->- sha256 ->- sha512 -- **output**: ->MISP attributes mapped from the result of the query on OTX, included in the following list: ->- domain ->- ip-src ->- ip-dst ->- text ->- md5 ->- sha1 ->- sha256 ->- sha512 ->- email -- **references**: ->https://www.alienvault.com/open-threat-exchange -- **requirements**: ->An access to the OTX API (apikey) - ------ - -#### [passivetotal](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/passivetotal.py) - - - - -- **features**: ->The PassiveTotal MISP expansion module brings the datasets derived from Internet scanning directly into your MISP instance. This module supports passive DNS, historic SSL, WHOIS, and host attributes. In order to use the module, you must have a valid PassiveTotal account username and API key. Registration is free and can be done by visiting https://www.passivetotal.org/register -- **input**: ->A MISP attribute included in the following list: ->- hostname ->- domain ->- ip-src ->- ip-dst ->- x509-fingerprint-sha1 ->- email-src ->- email-dst ->- target-email ->- whois-registrant-email ->- whois-registrant-phone ->- text ->- whois-registrant-name ->- whois-registrar ->- whois-creation-date -- **output**: ->MISP attributes mapped from the result of the query on PassiveTotal, included in the following list: ->- hostname ->- domain ->- ip-src ->- ip-dst ->- x509-fingerprint-sha1 ->- email-src ->- email-dst ->- target-email ->- whois-registrant-email ->- whois-registrant-phone ->- text ->- whois-registrant-name ->- whois-registrar ->- whois-creation-date ->- md5 ->- sha1 ->- sha256 ->- link -- **references**: ->https://www.passivetotal.org/register -- **requirements**: ->Passivetotal python library, An access to the PassiveTotal API (apikey) - ------ - -#### [rbl](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/rbl.py) - -Module to check an IPv4 address against known RBLs. -- **features**: ->This module takes an IP address attribute as input and queries multiple know Real-time Blackhost Lists to check if they have already seen this IP address. -> ->We display then all the information we get from those different sources. -- **input**: ->IP address attribute. -- **output**: ->Text with additional data from Real-time Blackhost Lists about the IP address. -- **references**: ->[RBLs list](https://github.com/MISP/misp-modules/blob/8817de476572a10a9c9d03258ec81ca70f3d926d/misp_modules/modules/expansion/rbl.py#L20) -- **requirements**: ->dnspython3: DNS python3 library - ------ - -#### [reversedns](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/reversedns.py) - -Simple Reverse DNS expansion service to resolve reverse DNS from MISP attributes. -- **features**: ->The module takes an IP address as input and tries to find the hostname this IP address is resolved into. -> ->The address of the DNS resolver to use is also configurable, but if no configuration is set, we use the Google public DNS address (8.8.8.8). -> ->Please note that composite MISP attributes containing IP addresses are supported as well. -- **input**: ->An IP address attribute. -- **output**: ->Hostname attribute the input is resolved into. -- **requirements**: ->DNS python library - ------ - -#### [securitytrails](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/securitytrails.py) - - - -An expansion modules for SecurityTrails. -- **features**: ->The module takes a domain, hostname or IP address attribute as input and queries the SecurityTrails API with it. -> ->Multiple parsing operations are then processed on the result of the query to extract a much information as possible. -> ->From this data extracted are then mapped MISP attributes. -- **input**: ->A domain, hostname or IP address attribute. -- **output**: ->MISP attributes resulting from the query on SecurityTrails API, included in the following list: ->- hostname ->- domain ->- ip-src ->- ip-dst ->- dns-soa-email ->- whois-registrant-email ->- whois-registrant-phone ->- whois-registrant-name ->- whois-registrar ->- whois-creation-date ->- domain -- **references**: ->https://securitytrails.com/ -- **requirements**: ->dnstrails python library, An access to the SecurityTrails API (apikey) - ------ - -#### [shodan](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/shodan.py) - - - -Module to query on Shodan. -- **features**: ->The module takes an IP address as input and queries the Shodan API to get some additional data about it. -- **input**: ->An IP address MISP attribute. -- **output**: ->Text with additional data about the input, resulting from the query on Shodan. -- **references**: ->https://www.shodan.io/ -- **requirements**: ->shodan python library, An access to the Shodan API (apikey) - ------ - -#### [sigma_queries](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/sigma_queries.py) - - - -An expansion hover module to display the result of sigma queries. -- **features**: ->This module takes a Sigma rule attribute as input and tries all the different queries available to convert it into different formats recognized by SIEMs. -- **input**: ->A Sigma attribute. -- **output**: ->Text displaying results of queries on the Sigma attribute. -- **references**: ->https://github.com/Neo23x0/sigma/wiki -- **requirements**: ->Sigma python library - ------ - -#### [sigma_syntax_validator](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/sigma_syntax_validator.py) - - - -An expansion hover module to perform a syntax check on sigma rules. -- **features**: ->This module takes a Sigma rule attribute as input and performs a syntax check on it. -> ->It displays then that the rule is valid if it is the case, and the error related to the rule otherwise. -- **input**: ->A Sigma attribute. -- **output**: ->Text describing the validity of the Sigma rule. -- **references**: ->https://github.com/Neo23x0/sigma/wiki -- **requirements**: ->Sigma python library, Yaml python library - ------ - -#### [sourcecache](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/sourcecache.py) - -Module to cache web pages of analysis reports, OSINT sources. The module returns a link of the cached page. -- **features**: ->This module takes a link or url attribute as input and caches the related web page. It returns then a link of the cached page. -- **input**: ->A link or url attribute. -- **output**: ->A malware-sample attribute describing the cached page. -- **references**: ->https://github.com/adulau/url_archiver -- **requirements**: ->urlarchiver: python library to fetch and archive URL on the file-system - ------ - -#### [stix2_pattern_syntax_validator](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/stix2_pattern_syntax_validator.py) - - - -An expansion hover module to perform a syntax check on stix2 patterns. -- **features**: ->This module takes a STIX2 pattern attribute as input and performs a syntax check on it. -> ->It displays then that the rule is valid if it is the case, and the error related to the rule otherwise. -- **input**: ->A STIX2 pattern attribute. -- **output**: ->Text describing the validity of the STIX2 pattern. -- **references**: ->[STIX2.0 patterning specifications](http://docs.oasis-open.org/cti/stix/v2.0/cs01/part5-stix-patterning/stix-v2.0-cs01-part5-stix-patterning.html) -- **requirements**: ->stix2patterns python library - ------ - -#### [threatcrowd](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/threatcrowd.py) - - - -Module to get information from ThreatCrowd. -- **features**: ->This module takes a MISP attribute as input and queries ThreatCrowd with it. -> ->The result of this query is then parsed and some data is mapped into MISP attributes in order to enrich the input attribute. -- **input**: ->A MISP attribute included in the following list: ->- hostname ->- domain ->- ip-src ->- ip-dst ->- md5 ->- sha1 ->- sha256 ->- sha512 ->- whois-registrant-email -- **output**: ->MISP attributes mapped from the result of the query on ThreatCrowd, included in the following list: ->- domain ->- ip-src ->- ip-dst ->- text ->- md5 ->- sha1 ->- sha256 ->- sha512 ->- hostname ->- whois-registrant-email -- **references**: ->https://www.threatcrowd.org/ - ------ - -#### [threatminer](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/threatminer.py) - - - -Module to get information from ThreatMiner. -- **features**: ->This module takes a MISP attribute as input and queries ThreatMiner with it. -> ->The result of this query is then parsed and some data is mapped into MISP attributes in order to enrich the input attribute. -- **input**: ->A MISP attribute included in the following list: ->- hostname ->- domain ->- ip-src ->- ip-dst ->- md5 ->- sha1 ->- sha256 ->- sha512 -- **output**: ->MISP attributes mapped from the result of the query on ThreatMiner, included in the following list: ->- domain ->- ip-src ->- ip-dst ->- text ->- md5 ->- sha1 ->- sha256 ->- sha512 ->- ssdeep ->- authentihash ->- filename ->- whois-registrant-email ->- url ->- link -- **references**: ->https://www.threatminer.org/ - ------ - -#### [urlscan](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/urlscan.py) - - - -An expansion module to query urlscan.io. -- **features**: ->This module takes a MISP attribute as input and queries urlscan.io with it. -> ->The result of this query is then parsed and some data is mapped into MISP attributes in order to enrich the input attribute. -- **input**: ->A domain, hostname or url attribute. -- **output**: ->MISP attributes mapped from the result of the query on urlscan.io. -- **references**: ->https://urlscan.io/ -- **requirements**: ->An access to the urlscan.io API - ------ - -#### [virustotal](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/virustotal.py) - - - -Module to get information from virustotal. -- **features**: ->This module takes a MISP attribute as input and queries the VirusTotal API with it, in order to get additional data on the input attribute. -> ->Multiple recursive requests on the API can then be processed on some attributes found in the first request. A limit can be set to restrict the number of values to query again, and at the same time the number of request submitted to the API. -> ->This limit is important because the default user VirusTotal apikey only allows to process a certain nunmber of queries per minute. As a consequence it is recommended to have a larger number of requests or a private apikey. -> ->Data is then mapped into MISP attributes. -- **input**: ->A domain, hash (md5, sha1, sha256 or sha512), hostname or IP address attribute. -- **output**: ->MISP attributes mapped from the rersult of the query on VirusTotal API. -- **references**: ->https://www.virustotal.com/ -- **requirements**: ->An access to the VirusTotal API (apikey) - ------ - -#### [vmray_submit](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/vmray_submit.py) - - - -Module to submit a sample to VMRay. -- **features**: ->This module takes an attachment or malware-sample attribute as input to query the VMRay API. -> ->The sample contained within the attribute in then enriched with data from VMRay mapped into MISP attributes. -- **input**: ->An attachment or malware-sample attribute. -- **output**: ->MISP attributes mapped from the result of the query on VMRay API, included in the following list: ->- text ->- sha1 ->- sha256 ->- md5 ->- link -- **references**: ->https://www.vmray.com/ -- **requirements**: ->An access to the VMRay API (apikey & url) - ------ - -#### [vulndb](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/vulndb.py) - - - -Module to query VulnDB (RiskBasedSecurity.com). -- **features**: ->This module takes a vulnerability attribute as input and queries VulnDB in order to get some additional data about it. -> ->The API gives the result of the query which can be displayed in the screen, and/or mapped into MISP attributes to add in the event. -- **input**: ->A vulnerability attribute. -- **output**: ->Additional data enriching the CVE input, fetched from VulnDB. -- **references**: ->https://vulndb.cyberriskanalytics.com/ -- **requirements**: ->An access to the VulnDB API (apikey, apisecret) - ------ - -#### [vulners](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/vulners.py) - - - -An expansion hover module to expand information about CVE id using Vulners API. -- **features**: ->This module takes a vulnerability attribute as input and queries the Vulners API in order to get some additional data about it. -> ->The API then returns details about the vulnerability. -- **input**: ->A vulnerability attribute. -- **output**: ->Text giving additional information about the CVE in input. -- **references**: ->https://vulners.com/ -- **requirements**: ->Vulners python library, An access to the Vulners API - ------ - -#### [whois](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/whois.py) - -Module to query a local instance of uwhois (https://github.com/rafiot/uwhoisd). -- **features**: ->This module takes a domain or IP address attribute as input and queries a 'Univseral Whois proxy server' to get the correct details of the Whois query on the input value (check the references for more details about this whois server). -- **input**: ->A domain or IP address attribute. -- **output**: ->Text describing the result of a whois request for the input value. -- **references**: ->https://github.com/rafiot/uwhoisd -- **requirements**: ->uwhois: A whois python library - ------ - -#### [wiki](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/wiki.py) - - - -An expansion hover module to extract information from Wikidata to have additional information about particular term for analysis. -- **features**: ->This module takes a text attribute as input and queries the Wikidata API. If the text attribute is clear enough to define a specific term, the API returns a wikidata link in response. -- **input**: ->Text attribute. -- **output**: ->Text attribute. -- **references**: ->https://www.wikidata.org -- **requirements**: ->SPARQLWrapper python library - ------ - -#### [xforceexchange](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/xforceexchange.py) - - - -An expansion module for IBM X-Force Exchange. -- **features**: ->This module takes a MISP attribute as input to query the X-Force API. The API returns then additional information known in their threats data, that is mapped into MISP attributes. -- **input**: ->A MISP attribute included in the following list: ->- ip-src ->- ip-dst ->- vulnerability ->- md5 ->- sha1 ->- sha256 -- **output**: ->MISP attributes mapped from the result of the query on X-Force Exchange. -- **references**: ->https://exchange.xforce.ibmcloud.com/ -- **requirements**: ->An access to the X-Force API (apikey) - ------ - -#### [yara_query](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/yara_query.py) - - - -An expansion & hover module to translate any hash attribute into a yara rule. -- **features**: ->The module takes a hash attribute (md5, sha1, sha256, imphash) as input, and is returning a YARA rule from it. This YARA rule is also validated using the same method as in 'yara_syntax_validator' module. ->Both hover and expansion functionalities are supported with this module, where the hover part is displaying the resulting YARA rule and the expansion part allows you to add the rule as a new attribute, as usual with expansion modules. -- **input**: ->MISP Hash attribute (md5, sha1, sha256, imphash, or any of the composite attribute with filename and one of the previous hash type). -- **output**: ->YARA rule. -- **references**: ->https://virustotal.github.io/yara/, https://github.com/virustotal/yara-python -- **requirements**: ->yara-python python library - ------ - -#### [yara_syntax_validator](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/yara_syntax_validator.py) - - - -An expansion hover module to perform a syntax check on if yara rules are valid or not. -- **features**: ->This modules simply takes a YARA rule as input, and checks its syntax. It returns then a confirmation if the syntax is valid, otherwise the syntax error is displayed. -- **input**: ->YARA rule attribute. -- **output**: ->Text to inform users if their rule is valid. -- **references**: ->http://virustotal.github.io/yara/ -- **requirements**: ->yara_python python library - ------ - -## Export Modules - -#### [cef_export](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/cef_export.py) - -Module to export a MISP event in CEF format. -- **features**: ->The module takes a MISP event in input, to look every attribute. Each attribute matching with some predefined types is then exported in Common Event Format. ->Thus, there is no particular feature concerning MISP Events since any event can be exported. However, 4 configuration parameters recognized by CEF format are required and should be provided by users before exporting data: the device vendor, product and version, as well as the default severity of data. -- **input**: ->MISP Event attributes -- **output**: ->Common Event Format file -- **references**: ->https://community.softwaregrp.com/t5/ArcSight-Connectors/ArcSight-Common-Event-Format-CEF-Guide/ta-p/1589306?attachment-id=65537 - ------ - -#### [goamlexport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/goamlexport.py) - - - -This module is used to export MISP events containing transaction objects into GoAML format. -- **features**: ->The module works as long as there is at least one transaction object in the Event. -> ->Then in order to have a valid GoAML document, please follow these guidelines: ->- For each transaction object, use either a bank-account, person, or legal-entity object to describe the origin of the transaction, and again one of them to describe the target of the transaction. ->- Create an object reference for both origin and target objects of the transaction. ->- A bank-account object needs a signatory, which is a person object, put as object reference of the bank-account. ->- A person can have an address, which is a geolocation object, put as object reference of the person. -> ->Supported relation types for object references that are recommended for each object are the folowing: ->- transaction: -> - 'from', 'from_my_client': Origin of the transaction - at least one of them is required. -> - 'to', 'to_my_client': Target of the transaction - at least one of them is required. -> - 'address': Location of the transaction - optional. ->- bank-account: -> - 'signatory': Signatory of a bank-account - the reference from bank-account to a signatory is required, but the relation-type is optional at the moment since this reference will always describe a signatory. -> - 'entity': Entity owning the bank account - optional. ->- person: -> - 'address': Address of a person - optional. -- **input**: ->MISP objects (transaction, bank-account, person, legal-entity, geolocation), with references, describing financial transactions and their origin and target. -- **output**: ->GoAML format file, describing financial transactions, with their origin and target (bank accounts, persons or entities). -- **references**: ->http://goaml.unodc.org/ -- **requirements**: ->PyMISP, MISP objects - ------ - -#### [liteexport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/liteexport.py) - -Lite export of a MISP event. -- **features**: ->This module is simply producing a json MISP event format file, but exporting only Attributes from the Event. Thus, MISP Events exported with this module should have attributes that are not internal references, otherwise the resulting event would be empty. -- **input**: ->MISP Event attributes -- **output**: ->Lite MISP Event - ------ - -#### [nexthinkexport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/nexthinkexport.py) - - - -Nexthink NXQL query export module -- **features**: ->This module export an event as Nexthink NXQL queries that can then be used in your own python3 tool or from wget/powershell -- **input**: ->MISP Event attributes -- **output**: ->Nexthink NXQL queries -- **references**: ->https://doc.nexthink.com/Documentation/Nexthink/latest/APIAndIntegrations/IntroducingtheWebAPIV2 - ------ - -#### [osqueryexport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/osqueryexport.py) - - - -OSQuery export of a MISP event. -- **features**: ->This module export an event as osquery queries that can be used in packs or in fleet management solution like Kolide. -- **input**: ->MISP Event attributes -- **output**: ->osquery SQL queries - ------ - -#### [pdfexport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/pdfexport.py) - -Simple export of a MISP event to PDF. -- **features**: ->The module takes care of the PDF file building, and work with any MISP Event. Except the requirement of asciidoctor, used to create the file, there is no special feature concerning the Event. -- **input**: ->MISP Event -- **output**: ->MISP Event in a PDF file. -- **references**: ->https://acrobat.adobe.com/us/en/acrobat/about-adobe-pdf.html -- **requirements**: ->PyMISP, asciidoctor - ------ - -#### [testexport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/testexport.py) - -Skeleton export module. - ------ - -#### [threatStream_misp_export](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/threatStream_misp_export.py) - - - -Module to export a structured CSV file for uploading to threatStream. -- **features**: ->The module takes a MISP event in input, to look every attribute. Each attribute matching with some predefined types is then exported in a CSV format recognized by ThreatStream. -- **input**: ->MISP Event attributes -- **output**: ->ThreatStream CSV format file -- **references**: ->https://www.anomali.com/platform/threatstream, https://github.com/threatstream -- **requirements**: ->csv - ------ - -#### [threat_connect_export](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/threat_connect_export.py) - - - -Module to export a structured CSV file for uploading to ThreatConnect. -- **features**: ->The module takes a MISP event in input, to look every attribute. Each attribute matching with some predefined types is then exported in a CSV format recognized by ThreatConnect. ->Users should then provide, as module configuration, the source of data they export, because it is required by the output format. -- **input**: ->MISP Event attributes -- **output**: ->ThreatConnect CSV format file -- **references**: ->https://www.threatconnect.com -- **requirements**: ->csv - ------ - -## Import Modules - -#### [csvimport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/csvimport.py) - -Module to import MISP attributes from a csv file. -- **features**: ->In order to parse data from a csv file, a header is required to let the module know which column is matching with known attribute fields / MISP types. ->This header is part of the configuration of the module and should be filled out in MISP plugin settings, each field separated by COMMAS. Fields that do not match with any type known in MISP can be ignored in import, using a space or simply nothing between two separators (example: 'ip-src, , comment, '). ->There is also one type that is confused and can be either a MISP attribute type or an attribute field: 'comment'. In this case, using 'attrComment' specifies that the attribute field 'comment' should be considered, otherwise it will be considered as the MISP attribute type. -> ->For each MISP attribute type, an attribute is created. ->Attribute fields that are imported are the following: value, type, category, to-ids, distribution, comment, tag. -- **input**: ->CSV format file. -- **output**: ->MISP Event attributes -- **references**: ->https://tools.ietf.org/html/rfc4180, https://tools.ietf.org/html/rfc7111 -- **requirements**: ->PyMISP - ------ - -#### [cuckooimport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/cuckooimport.py) - - - -Module to import Cuckoo JSON. -- **features**: ->The module simply imports MISP Attributes from a Cuckoo JSON format file. There is thus no special feature to make it work. -- **input**: ->Cuckoo JSON file -- **output**: ->MISP Event attributes -- **references**: ->https://cuckoosandbox.org/, https://github.com/cuckoosandbox/cuckoo - ------ - -#### [email_import](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/email_import.py) - -Module to import emails in MISP. -- **features**: ->This module can be used to import e-mail text as well as attachments and urls. ->3 configuration parameters are then used to unzip attachments, guess zip attachment passwords, and extract urls: set each one of them to True or False to process or not the respective corresponding actions. -- **input**: ->E-mail file -- **output**: ->MISP Event attributes - ------ - -#### [goamlimport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/goamlimport.py) - - - -Module to import MISP objects about financial transactions from GoAML files. -- **features**: ->Unlike the GoAML export module, there is here no special feature to import data from GoAML external files, since the module will import MISP Objects with their References on its own, as it is required for the export module to rebuild a valid GoAML document. -- **input**: ->GoAML format file, describing financial transactions, with their origin and target (bank accounts, persons or entities). -- **output**: ->MISP objects (transaction, bank-account, person, legal-entity, geolocation), with references, describing financial transactions and their origin and target. -- **references**: ->http://goaml.unodc.org/ -- **requirements**: ->PyMISP - ------ - -#### [mispjson](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/mispjson.py) - -Module to import MISP JSON format for merging MISP events. -- **features**: ->The module simply imports MISP Attributes from an other MISP Event in order to merge events together. There is thus no special feature to make it work. -- **input**: ->MISP Event -- **output**: ->MISP Event attributes - ------ - -#### [ocr](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/ocr.py) - -Optical Character Recognition (OCR) module for MISP. -- **features**: ->The module tries to recognize some text from an image and import the result as a freetext attribute, there is then no special feature asked to users to make it work. -- **input**: ->Image -- **output**: ->freetext MISP attribute - ------ - -#### [openiocimport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/openiocimport.py) - -Module to import OpenIOC packages. -- **features**: ->The module imports MISP Attributes from OpenIOC packages, there is then no special feature for users to make it work. -- **input**: ->OpenIOC packages -- **output**: ->MISP Event attributes -- **references**: ->https://www.fireeye.com/blog/threat-research/2013/10/openioc-basics.html -- **requirements**: ->PyMISP - ------ - -#### [threatanalyzer_import](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/threatanalyzer_import.py) - -Module to import ThreatAnalyzer archive.zip / analysis.json files. -- **features**: ->The module imports MISP Attributes from a ThreatAnalyzer format file. This file can be either ZIP, or JSON format. ->There is by the way no special feature for users to make the module work. -- **input**: ->ThreatAnalyzer format file -- **output**: ->MISP Event attributes -- **references**: ->https://www.threattrack.com/malware-analysis.aspx - ------ - -#### [vmray_import](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/vmray_import.py) - - - -Module to import VMRay (VTI) results. -- **features**: ->The module imports MISP Attributes from VMRay format, using the VMRay api. ->Users should then provide as the module configuration the API Key as well as the server url in order to fetch their data to import. -- **input**: ->VMRay format -- **output**: ->MISP Event attributes -- **references**: ->https://www.vmray.com/ -- **requirements**: ->vmray_rest_api - ------ diff --git a/doc/expansion/btc_scam_check.json b/doc/expansion/btc_scam_check.json new file mode 100644 index 0000000..44fce03 --- /dev/null +++ b/doc/expansion/btc_scam_check.json @@ -0,0 +1,9 @@ +{ + "description": "An expansion hover module to query a special dns blacklist to check if a bitcoin address has been abused.", + "requirements": ["dnspython3: dns python library"], + "features": "The module queries a dns blacklist directly with the bitcoin address and get a response if the address has been abused.", + "logo": "logos/bitcoin.png", + "input": "btc address attribute.", + "output" : "Text to indicate if the BTC address has been abused.", + "references": ["https://btcblack.it/"] +} diff --git a/doc/expansion/btc.json b/doc/expansion/btc_steroids.json similarity index 100% rename from doc/expansion/btc.json rename to doc/expansion/btc_steroids.json diff --git a/doc/generate_documentation.py b/doc/generate_documentation.py index 980ddf6..caef84e 100644 --- a/doc/generate_documentation.py +++ b/doc/generate_documentation.py @@ -30,7 +30,7 @@ def generate_doc(root_path): value = ', '.join(value) if isinstance(value, list) else '{}'.format(value.replace('\n', '\n>')) markdown.append('- **{}**:\n>{}\n'.format(field, value)) markdown.append('\n-----\n') - with open('documentation.md', 'w') as w: + with open('README.md', 'w') as w: w.write(''.join(markdown)) diff --git a/misp_modules/modules/expansion/__init__.py b/misp_modules/modules/expansion/__init__.py index 559e5aa..2507226 100644 --- a/misp_modules/modules/expansion/__init__.py +++ b/misp_modules/modules/expansion/__init__.py @@ -8,4 +8,4 @@ __all__ = ['vmray_submit', 'bgpranking', 'circl_passivedns', 'circl_passivessl', 'yara_syntax_validator', 'hashdd', 'onyphe', 'onyphe_full', 'rbl', 'xforceexchange', 'sigma_syntax_validator', 'stix2_pattern_syntax_validator', 'sigma_queries', 'dbl_spamhaus', 'vulners', 'yara_query', 'macaddress_io', - 'intel471'] + 'intel471', 'btc_scam_check'] diff --git a/misp_modules/modules/expansion/btc_scam_check.py b/misp_modules/modules/expansion/btc_scam_check.py new file mode 100644 index 0000000..b49414d --- /dev/null +++ b/misp_modules/modules/expansion/btc_scam_check.py @@ -0,0 +1,43 @@ +import json +import sys + +try: + from dns.resolver import Resolver, NXDOMAIN + from dns.name import LabelTooLong + resolver = Resolver() + resolver.timeout = 1 + resolver.lifetime = 1 +except ImportError: + sys.exit("dnspython3 in missing. use 'pip install dnspython3' to install it.") + +misperrors = {'error': 'Error'} +mispattributes = {'input': ['btc'], 'output': ['text']} +moduleinfo = {'version': '0.1', 'author': 'Christian Studer', + 'description': 'Checks if a BTC address is referenced as a scam.', + 'module-type': ['hover']} +moduleconfig = [] + +url = 'bl.btcblack.it' + +def handler(q=False): + if q is False: + return False + request = json.loads(q) + btc = request['btc'] + query = f"{btc}.{url}" + try: + result = ' - '.join([str(r) for r in resolver.query(query, 'TXT')])[1:-1] + except NXDOMAIN: + result = f"{btc} is not known as a scam address." + except LabelTooLong: + result = f"{btc} is probably not a valid BTC address." + return {'results': [{'types': mispattributes['output'], 'values': result}]} + + +def introspection(): + return mispattributes + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo From 08fe0cbe09e73e866f75fb348ef20f6c5e363d7f Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Tue, 5 Feb 2019 14:54:22 +0100 Subject: [PATCH 006/207] fix: Description fixed --- misp_modules/modules/expansion/btc_scam_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/btc_scam_check.py b/misp_modules/modules/expansion/btc_scam_check.py index b49414d..9f9a7d6 100644 --- a/misp_modules/modules/expansion/btc_scam_check.py +++ b/misp_modules/modules/expansion/btc_scam_check.py @@ -13,7 +13,7 @@ except ImportError: misperrors = {'error': 'Error'} mispattributes = {'input': ['btc'], 'output': ['text']} moduleinfo = {'version': '0.1', 'author': 'Christian Studer', - 'description': 'Checks if a BTC address is referenced as a scam.', + 'description': 'Checks if a BTC address has been abused.', 'module-type': ['hover']} moduleconfig = [] From e4c14689683c5b069e7707f578e80499de8b0997 Mon Sep 17 00:00:00 2001 From: 9b Date: Fri, 8 Feb 2019 12:27:20 -0500 Subject: [PATCH 007/207] Stubbed module --- README.md | 1 + REQUIREMENTS | 1 + misp_modules/modules/expansion/__init__.py | 2 +- .../modules/expansion/backscatter_io.py | 77 +++++++++++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 misp_modules/modules/expansion/backscatter_io.py diff --git a/README.md b/README.md index 368ef6f..59f2346 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ ### Expansion modules +* [Backscatter.io](misp_modules/modules/expansion/backscatter_io) - a hover and expansion module to expand an IP address with mass-scanning observations. * [BGP Ranking](misp_modules/modules/expansion/bgpranking.py) - a hover and expansion module to expand an AS number with the ASN description, its history, and position in BGP Ranking. * [BTC transactions](misp_modules/modules/expansion/btc_steroids.py) - An expansion hover module to get a blockchain balance and the transactions from a BTC address in MISP. * [CIRCL Passive DNS](misp_modules/modules/expansion/circl_passivedns.py) - a hover and expansion module to expand hostname and IP addresses with passive DNS information. diff --git a/REQUIREMENTS b/REQUIREMENTS index c3c16e6..0720e90 100644 --- a/REQUIREMENTS +++ b/REQUIREMENTS @@ -11,6 +11,7 @@ aiohttp==3.4.4 antlr4-python3-runtime==4.7.2 ; python_version >= '3' async-timeout==3.0.1 attrs==18.2.0 +backscatter==0.2.3 beautifulsoup4==4.7.1 blockchain==1.4.4 certifi==2018.11.29 diff --git a/misp_modules/modules/expansion/__init__.py b/misp_modules/modules/expansion/__init__.py index 559e5aa..b6bc74d 100644 --- a/misp_modules/modules/expansion/__init__.py +++ b/misp_modules/modules/expansion/__init__.py @@ -8,4 +8,4 @@ __all__ = ['vmray_submit', 'bgpranking', 'circl_passivedns', 'circl_passivessl', 'yara_syntax_validator', 'hashdd', 'onyphe', 'onyphe_full', 'rbl', 'xforceexchange', 'sigma_syntax_validator', 'stix2_pattern_syntax_validator', 'sigma_queries', 'dbl_spamhaus', 'vulners', 'yara_query', 'macaddress_io', - 'intel471'] + 'intel471', 'backscatter_io'] diff --git a/misp_modules/modules/expansion/backscatter_io.py b/misp_modules/modules/expansion/backscatter_io.py new file mode 100644 index 0000000..2af073e --- /dev/null +++ b/misp_modules/modules/expansion/backscatter_io.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +"""Backscatter.io Module.""" +import json +try: + from backscatter import Backscatter +except ImportError: + print("Backscatter.io library not installed.") + +misperrors = {'error': 'Error'} +mispattributes = {'input': ['ip-src', 'ip-dst'], 'output': ['freetext']} +moduleinfo = {'version': '1', 'author': 'brandon@backscatter.io', + 'description': 'Backscatter.io module to bring mass-scanning observations into MISP.', + 'module-type': ['expansion', 'hover']} +moduleconfig = ['api_key'] +query_playbook = [ + {'inputs': ['ip-src', 'ip-dst'], + 'services': ['observations', 'enrichment'], + 'name': 'generic'} +] + + +def check_query(request): + """Check the incoming request for a valid configuration.""" + output = {'success': False} + config = request.get('config', None) + if not config: + misperrors['error'] = "Configuration is missing from the request." + return output + for item in moduleconfig: + if config.get(item, None): + continue + misperrors['error'] = "Backscatter.io authentication is missing." + return output + if not request.get('ip-src') and request.get('ip-dst'): + misperrors['error'] = "Unsupported attributes type." + return output + profile = {'success': True, 'config': config, 'playbook': 'generic'} + if 'ip-src' in request: + profile.update({'value': request.get('ip-src')}) + else: + profile.update({'value': request.get('ip-dst')}) + return profile + + +def handler(q=False): + """Handle gathering data.""" + if not q: + return q + request = json.loads(q) + checks = check_query(request) + if not checks['success']: + return misperrors + + output = {'results': list()} + + try: + bs = Backscatter(checks['config']['api_key']) + response = bs.get_observations(query=output['value'], query_type='ip') + if not response['success']: + misperrors['error'] = '%s: %s' % (response['error'], response['message']) + return misperrors + r = {'results': [{'types': mispattributes['output'], 'values': [str(response)]}]} + except Exception, e: + misperrors['error'] = str(e) + return misperrors + + return output + + +def introspection(): + return mispattributes + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo + From c8b410161a19f0edbaedefd80d860ddc0cab326d Mon Sep 17 00:00:00 2001 From: 9b Date: Fri, 8 Feb 2019 12:29:43 -0500 Subject: [PATCH 008/207] Use the write var on return --- misp_modules/modules/expansion/backscatter_io.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/misp_modules/modules/expansion/backscatter_io.py b/misp_modules/modules/expansion/backscatter_io.py index 2af073e..dab07a7 100644 --- a/misp_modules/modules/expansion/backscatter_io.py +++ b/misp_modules/modules/expansion/backscatter_io.py @@ -51,15 +51,13 @@ def handler(q=False): if not checks['success']: return misperrors - output = {'results': list()} - try: bs = Backscatter(checks['config']['api_key']) response = bs.get_observations(query=output['value'], query_type='ip') if not response['success']: misperrors['error'] = '%s: %s' % (response['error'], response['message']) return misperrors - r = {'results': [{'types': mispattributes['output'], 'values': [str(response)]}]} + output = {'results': [{'types': mispattributes['output'], 'values': [str(response)]}]} except Exception, e: misperrors['error'] = str(e) return misperrors From acc35e3a02362b2cddfdec31d48a0c740e4ecbac Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Sun, 10 Feb 2019 16:33:09 +0100 Subject: [PATCH 009/207] chg: [backscatter.io] Exception handler fixed for recent version of Python --- misp_modules/modules/expansion/backscatter_io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/backscatter_io.py b/misp_modules/modules/expansion/backscatter_io.py index dab07a7..5a8a9cd 100644 --- a/misp_modules/modules/expansion/backscatter_io.py +++ b/misp_modules/modules/expansion/backscatter_io.py @@ -58,7 +58,7 @@ def handler(q=False): misperrors['error'] = '%s: %s' % (response['error'], response['message']) return misperrors output = {'results': [{'types': mispattributes['output'], 'values': [str(response)]}]} - except Exception, e: + except Exception as e: misperrors['error'] = str(e) return misperrors From 7b1a837b109f0ff1a8c9240bb31a84753739f438 Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Sun, 10 Feb 2019 16:40:06 +0100 Subject: [PATCH 010/207] chg: [backscatter.io] remove blank line at the end of the file --- misp_modules/modules/expansion/backscatter_io.py | 1 - 1 file changed, 1 deletion(-) diff --git a/misp_modules/modules/expansion/backscatter_io.py b/misp_modules/modules/expansion/backscatter_io.py index 5a8a9cd..bfa04f6 100644 --- a/misp_modules/modules/expansion/backscatter_io.py +++ b/misp_modules/modules/expansion/backscatter_io.py @@ -72,4 +72,3 @@ def introspection(): def version(): moduleinfo['config'] = moduleconfig return moduleinfo - From 30753d57aa3096ce85c3ed54eed6e0f2dc3d054c Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Sun, 10 Feb 2019 16:46:43 +0100 Subject: [PATCH 011/207] chg: [doc] backscatter.io documentation added --- doc/expansion/backscatter_io.json | 8 ++++++++ doc/logos/backscatter_io.png | Bin 0 -> 25590 bytes 2 files changed, 8 insertions(+) create mode 100644 doc/expansion/backscatter_io.json create mode 100644 doc/logos/backscatter_io.png diff --git a/doc/expansion/backscatter_io.json b/doc/expansion/backscatter_io.json new file mode 100644 index 0000000..22123a5 --- /dev/null +++ b/doc/expansion/backscatter_io.json @@ -0,0 +1,8 @@ +{ + "description": "Query backscatter.io (https://backscatter.io/).", + "requirements": ["backscatter python library"], + "features": "The module takes a source or destination IP address as input and displays the information known by backscatter.io.\n\n", + "references": ["https://pypi.org/project/backscatter/"], + "input": "IP addresses.", + "output": "Text containing a history of the IP addresses especially on scanning based on backscatter.io information ." +} diff --git a/doc/logos/backscatter_io.png b/doc/logos/backscatter_io.png new file mode 100644 index 0000000000000000000000000000000000000000..0973112835e7f4ce67375c3c2b51e2fc23628175 GIT binary patch literal 25590 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 Date: Sun, 10 Feb 2019 16:47:42 +0100 Subject: [PATCH 012/207] chg: [doc] backscatter.io updated --- doc/documentation.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/doc/documentation.md b/doc/documentation.md index 31f09ed..23b4bce 100644 --- a/doc/documentation.md +++ b/doc/documentation.md @@ -2,6 +2,24 @@ ## Expansion Modules +#### [backscatter_io](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/backscatter_io.py) + +Query backscatter.io (https://backscatter.io/). +- **features**: +>The module takes a source or destination IP address as input and displays the information known by backscatter.io. +> +> +- **input**: +>IP addresses. +- **output**: +>Text containing a history of the IP addresses especially on scanning based on backscatter.io information . +- **references**: +>https://pypi.org/project/backscatter/ +- **requirements**: +>backscatter python library + +----- + #### [bgpranking](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/bgpranking.py) Query BGP Ranking (https://bgpranking-ng.circl.lu/). From f0ccfd2027f19378c681ab4d5f79191bae3c43cb Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Sun, 10 Feb 2019 16:56:01 +0100 Subject: [PATCH 013/207] chg: [backscatter.io] blind fix regarding undefined value --- misp_modules/modules/expansion/backscatter_io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/backscatter_io.py b/misp_modules/modules/expansion/backscatter_io.py index bfa04f6..0796917 100644 --- a/misp_modules/modules/expansion/backscatter_io.py +++ b/misp_modules/modules/expansion/backscatter_io.py @@ -53,7 +53,7 @@ def handler(q=False): try: bs = Backscatter(checks['config']['api_key']) - response = bs.get_observations(query=output['value'], query_type='ip') + response = bs.get_observations(query=checks['value'], query_type='ip') if not response['success']: misperrors['error'] = '%s: %s' % (response['error'], response['message']) return misperrors From 61c0274f78907170d50dfef5c2659b6e4b35c3ae Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 11 Feb 2019 09:32:53 +0100 Subject: [PATCH 014/207] fix: Regenerated documentation --- doc/README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/doc/README.md b/doc/README.md index e47470d..44bb5b0 100644 --- a/doc/README.md +++ b/doc/README.md @@ -2,6 +2,24 @@ ## Expansion Modules +#### [backscatter_io](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/backscatter_io.py) + +Query backscatter.io (https://backscatter.io/). +- **features**: +>The module takes a source or destination IP address as input and displays the information known by backscatter.io. +> +> +- **input**: +>IP addresses. +- **output**: +>Text containing a history of the IP addresses especially on scanning based on backscatter.io information . +- **references**: +>https://pypi.org/project/backscatter/ +- **requirements**: +>backscatter python library + +----- + #### [bgpranking](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/bgpranking.py) Query BGP Ranking (https://bgpranking-ng.circl.lu/). From e940070956432cd7aaba0c4a8fad9c49839792e2 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 11 Feb 2019 09:43:25 +0100 Subject: [PATCH 015/207] add: [doc] Added backscatter.io logo + regenerated documentation --- doc/README.md | 2 ++ doc/expansion/backscatter_io.json | 1 + 2 files changed, 3 insertions(+) diff --git a/doc/README.md b/doc/README.md index 44bb5b0..c32a8c4 100644 --- a/doc/README.md +++ b/doc/README.md @@ -4,6 +4,8 @@ #### [backscatter_io](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/backscatter_io.py) + + Query backscatter.io (https://backscatter.io/). - **features**: >The module takes a source or destination IP address as input and displays the information known by backscatter.io. diff --git a/doc/expansion/backscatter_io.json b/doc/expansion/backscatter_io.json index 22123a5..a8475c5 100644 --- a/doc/expansion/backscatter_io.json +++ b/doc/expansion/backscatter_io.json @@ -2,6 +2,7 @@ "description": "Query backscatter.io (https://backscatter.io/).", "requirements": ["backscatter python library"], "features": "The module takes a source or destination IP address as input and displays the information known by backscatter.io.\n\n", + "logo": "logos/backscatter_io.png", "references": ["https://pypi.org/project/backscatter/"], "input": "IP addresses.", "output": "Text containing a history of the IP addresses especially on scanning based on backscatter.io information ." From 0bf27c1b69b12ea4bbefad830a89791d558eca07 Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Mon, 11 Feb 2019 14:23:18 +0100 Subject: [PATCH 016/207] chg: [btc_scam_check] fix spacing for making flake 8 happy --- misp_modules/modules/expansion/btc_scam_check.py | 1 + 1 file changed, 1 insertion(+) diff --git a/misp_modules/modules/expansion/btc_scam_check.py b/misp_modules/modules/expansion/btc_scam_check.py index 9f9a7d6..f551926 100644 --- a/misp_modules/modules/expansion/btc_scam_check.py +++ b/misp_modules/modules/expansion/btc_scam_check.py @@ -19,6 +19,7 @@ moduleconfig = [] url = 'bl.btcblack.it' + def handler(q=False): if q is False: return False From 9abc3a4b0a323cdd2617be673847422ad8d39fb5 Mon Sep 17 00:00:00 2001 From: iwitz Date: Fri, 15 Feb 2019 10:16:52 +0100 Subject: [PATCH 017/207] add: rhel installation instructions --- README.md | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b60accd..ee4f2f8 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ sudo sed -i -e '$i \sudo -u www-data /var/www/MISP/venv/bin/misp-modules -l 127. /var/www/MISP/venv/bin/misp-modules -l 127.0.0.1 -s & #to start the modules ~~~~ -## How to install and start MISP modules? +## How to install and start MISP modules on Debian-based distributions ? ~~~~bash sudo apt-get install python3-dev python3-pip libpq5 libjpeg-dev tesseract-ocr imagemagick @@ -115,6 +115,42 @@ sudo sed -i -e '$i \sudo -u www-data /var/www/MISP/venv/bin/misp-modules -l 127. /var/www/MISP/venv/bin/misp-modules -l 127.0.0.1 -s & #to start the modules ~~~~ +## How to install and start MISP modules on RHEL-based distributions ? +As of this writing, the official RHEL repositories only contain Ruby 2.0.0 and Ruby 2.1 or higher is required. As such, this guide installs Ruby 2.2 from the [SCL](https://access.redhat.com/documentation/en-us/red_hat_software_collections/3/html/3.2_release_notes/chap-installation#sect-Installation-Subscribe) repository. +~~~~bash +yum install rh-ruby22 +cd /var/www/MISP +git clone https://github.com/MISP/misp-modules.git +cd misp-modules +scl enable rh-python36 ‘python3 –m pip install cryptography’ +scl enable rh-python36 ‘python3 –m pip install -I -r REQUIREMENTS’ +scl enable rh-python36 ‘python3 –m pip install –I .’ +scl enable rh-ruby22 ‘gem install asciidoctor-pdf –pre’ +~~~~ +Create the service file /etc/systemd/system/misp-workers.service : +~~~~ +[Unit] +Description=MISP's modules +After=misp-workers.service + +[Service] +Type=simple +User=apache +Group=apache +ExecStart=/usr/bin/scl enable rh-python36 rh-ruby22 ‘/opt/rh/rh-python36/root/bin/misp-modules –l 127.0.0.1 –s’ +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +~~~~ +The `After=misp-workers.service` must be changed or removed if you have not created a misp-workers service. +Then, enable the misp-modules service and start it ; +~~~~bash +systemctl daemon-reload +systemctl enable --now misp-modules +~~~~ + ## How to add your own MISP modules? Create your module in [misp_modules/modules/expansion/](misp_modules/modules/expansion/), [misp_modules/modules/export_mod/](misp_modules/modules/export_mod/), or [misp_modules/modules/import_mod/](misp_modules/modules/import_mod/). The module should have at minimum three functions: From 2753f354ab3291985b8beb9aa745fe30d1e6b2a8 Mon Sep 17 00:00:00 2001 From: Vincent-CIRCL Date: Mon, 18 Feb 2019 14:27:16 +0100 Subject: [PATCH 018/207] test update --- misp_modules/modules/export_mod/pdfexport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/export_mod/pdfexport.py b/misp_modules/modules/export_mod/pdfexport.py index df7f879..1b4c731 100755 --- a/misp_modules/modules/export_mod/pdfexport.py +++ b/misp_modules/modules/export_mod/pdfexport.py @@ -12,7 +12,7 @@ from pymisp import MISPEvent misperrors = {'error': 'Error'} -moduleinfo = {'version': '1', +moduleinfo = {'version': '42', 'author': 'Raphaël Vinot', 'description': 'Simple export to PDF', 'module-type': ['export'], From be01d547791f7f9950dad170b3908155e15ff3c8 Mon Sep 17 00:00:00 2001 From: Vincent-CIRCL Date: Mon, 18 Feb 2019 15:23:57 +0100 Subject: [PATCH 019/207] print values --- misp_modules/modules/export_mod/pdfexport.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/misp_modules/modules/export_mod/pdfexport.py b/misp_modules/modules/export_mod/pdfexport.py index 1b4c731..074e473 100755 --- a/misp_modules/modules/export_mod/pdfexport.py +++ b/misp_modules/modules/export_mod/pdfexport.py @@ -144,6 +144,8 @@ def handler(q=False): return False for evt in request['data']: + print(request['data']) + report = ReportGenerator() report.report_headers() report.from_event(evt) From 5d824a24f9f1348f78da8e6a5e7c61329db26bb8 Mon Sep 17 00:00:00 2001 From: Steve Clement Date: Thu, 21 Feb 2019 10:06:28 +0530 Subject: [PATCH 020/207] chg: [pypi] Made sure url-normalize installs less stric. --- REQUIREMENTS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/REQUIREMENTS b/REQUIREMENTS index 0720e90..7c8dde3 100644 --- a/REQUIREMENTS +++ b/REQUIREMENTS @@ -54,7 +54,8 @@ soupsieve==1.7.3 sparqlwrapper==1.8.2 stix2-patterns==1.1.0 tornado==5.1.1 -url-normalize==1.4.1 +# Python 3.5 only provides url-normalize v1.3.3 +url-normalize>=1.3.3 urlarchiver==0.2 urllib3==1.24.1 vulners==1.4.0 From 2d29ce11bbf8dadd82fe886330476488df32c140 Mon Sep 17 00:00:00 2001 From: Falconieri Date: Thu, 21 Feb 2019 15:42:18 +0100 Subject: [PATCH 021/207] Test 1 - PDF call --- misp_modules/modules/export_mod/pdfexport.py | 68 +++++++------------- 1 file changed, 22 insertions(+), 46 deletions(-) diff --git a/misp_modules/modules/export_mod/pdfexport.py b/misp_modules/modules/export_mod/pdfexport.py index 074e473..23d0edd 100755 --- a/misp_modules/modules/export_mod/pdfexport.py +++ b/misp_modules/modules/export_mod/pdfexport.py @@ -7,61 +7,26 @@ import shlex import subprocess import base64 -from pymisp import MISPEvent +from pymisp import MISPEvent, reportlab_generator misperrors = {'error': 'Error'} -moduleinfo = {'version': '42', - 'author': 'Raphaël Vinot', +moduleinfo = {'version': '2', + 'author': 'Vincent Falconieri (prev. Raphaël Vinot)', 'description': 'Simple export to PDF', 'module-type': ['export'], 'require_standard_format': True} moduleconfig = [] - mispattributes = {} + outputFileExtension = "pdf" responseType = "application/pdf" types_to_attach = ['ip-dst', 'url', 'domain'] objects_to_attach = ['domain-ip'] -headers = """ -:toc: right -:toclevels: 1 -:toc-title: Daily Report -:icons: font -:sectanchors: -:sectlinks: -= Daily report by {org_name} -{date} - -:icons: font - -""" - -event_level_tags = """ -IMPORTANT: This event is classified TLP:{value}. - -{expanded} - -""" - -attributes = """ -=== Indicator(s) of compromise - -{list_attributes} - -""" - -title = """ -== ({internal_id}) {title} - -{summary} - -""" - class ReportGenerator(): def __init__(self): @@ -79,6 +44,9 @@ class ReportGenerator(): self.misp_event = MISPEvent() self.misp_event.load(event) + ''' + + def attributes(self): if not self.misp_event.attributes: return '' @@ -132,7 +100,7 @@ class ReportGenerator(): self.report += self.title() self.report += self.event_level_tags() self.report += self.attributes() - + ''' def handler(q=False): if q is False: @@ -144,19 +112,27 @@ def handler(q=False): return False for evt in request['data']: + + ''' + print(" DATA ") print(request['data']) + + reportlab_generator. report = ReportGenerator() report.report_headers() report.from_event(evt) report.asciidoc() - command_line = 'asciidoctor-pdf -' - args = shlex.split(command_line) - with subprocess.Popen(args, stdout=subprocess.PIPE, stdin=subprocess.PIPE) as process: - cmd_out, cmd_err = process.communicate( - input=report.report.encode('utf-8')) - return {'response': [], 'data': str(base64.b64encode(cmd_out), 'utf-8')} + print(" REPORT : ") + print(report) + ''' + misp_event = MISPEvent() + misp_event.load(request['data']) + + pdf = reportlab_generator.get_base64_from_buffer(reportlab_generator.convert_event_in_pdf_buffer(misp_event)) + + return {'response': [], 'data': str(pdf, 'utf-8')} def introspection(): From a93b34208f358c76184b725f656f43f39dbd8e18 Mon Sep 17 00:00:00 2001 From: Falconieri Date: Fri, 22 Feb 2019 10:14:22 +0100 Subject: [PATCH 022/207] fix: [pdfexport] Bugfix on PyMisp exportpdf call --- misp_modules/modules/export_mod/pdfexport.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/misp_modules/modules/export_mod/pdfexport.py b/misp_modules/modules/export_mod/pdfexport.py index 23d0edd..cb4e297 100755 --- a/misp_modules/modules/export_mod/pdfexport.py +++ b/misp_modules/modules/export_mod/pdfexport.py @@ -7,8 +7,10 @@ import shlex import subprocess import base64 -from pymisp import MISPEvent, reportlab_generator +print("test PDF pdf export (reportlab generator import)") +from pymisp import MISPEvent +from pymisp.tools import reportlab_generator misperrors = {'error': 'Error'} @@ -45,8 +47,6 @@ class ReportGenerator(): self.misp_event.load(event) ''' - - def attributes(self): if not self.misp_event.attributes: return '' @@ -127,12 +127,13 @@ def handler(q=False): print(" REPORT : ") print(report) ''' - misp_event = MISPEvent() - misp_event.load(request['data']) - pdf = reportlab_generator.get_base64_from_buffer(reportlab_generator.convert_event_in_pdf_buffer(misp_event)) + misp_event = MISPEvent() + misp_event.load(evt) - return {'response': [], 'data': str(pdf, 'utf-8')} + pdf = reportlab_generator.get_base64_from_value(reportlab_generator.convert_event_in_pdf_buffer(misp_event)) + + return {'response': [], 'data': str(pdf, 'utf-8')} def introspection(): @@ -164,3 +165,8 @@ def introspection(): def version(): moduleinfo['config'] = moduleconfig return moduleinfo + +import pprint + +if __name__ == "__main__": + pprint.pprint("test") \ No newline at end of file From 40cd32f1b8c073caf20d133ca0e9780ec2f602cc Mon Sep 17 00:00:00 2001 From: Falconieri Date: Fri, 22 Feb 2019 10:25:12 +0100 Subject: [PATCH 023/207] tidy: Remove old dead export code --- misp_modules/modules/export_mod/pdfexport.py | 78 -------------------- 1 file changed, 78 deletions(-) diff --git a/misp_modules/modules/export_mod/pdfexport.py b/misp_modules/modules/export_mod/pdfexport.py index cb4e297..ef3d775 100755 --- a/misp_modules/modules/export_mod/pdfexport.py +++ b/misp_modules/modules/export_mod/pdfexport.py @@ -7,8 +7,6 @@ import shlex import subprocess import base64 -print("test PDF pdf export (reportlab generator import)") - from pymisp import MISPEvent from pymisp.tools import reportlab_generator @@ -46,62 +44,6 @@ class ReportGenerator(): self.misp_event = MISPEvent() self.misp_event.load(event) - ''' - def attributes(self): - if not self.misp_event.attributes: - return '' - list_attributes = [] - for attribute in self.misp_event.attributes: - if attribute.type in types_to_attach: - list_attributes.append("* {}".format(attribute.value)) - for obj in self.misp_event.Object: - if obj.name in objects_to_attach: - for attribute in obj.Attribute: - if attribute.type in types_to_attach: - list_attributes.append("* {}".format(attribute.value)) - return attributes.format(list_attributes="\n".join(list_attributes)) - - def _get_tag_info(self, machinetag): - return self.taxonomies.revert_machinetag(machinetag) - - def report_headers(self): - content = {'org_name': 'name', - 'date': date.today().isoformat()} - self.report += headers.format(**content) - - def event_level_tags(self): - if not self.misp_event.Tag: - return '' - for tag in self.misp_event.Tag: - # Only look for TLP for now - if tag['name'].startswith('tlp'): - tax, predicate = self._get_tag_info(tag['name']) - return self.event_level_tags.format(value=predicate.predicate.upper(), expanded=predicate.expanded) - - def title(self): - internal_id = '' - summary = '' - # Get internal refs for report - if not hasattr(self.misp_event, 'Object'): - return '' - for obj in self.misp_event.Object: - if obj.name != 'report': - continue - for a in obj.Attribute: - if a.object_relation == 'case-number': - internal_id = a.value - if a.object_relation == 'summary': - summary = a.value - - return title.format(internal_id=internal_id, title=self.misp_event.info, - summary=summary) - - def asciidoc(self, lang='en'): - self.report += self.title() - self.report += self.event_level_tags() - self.report += self.attributes() - ''' - def handler(q=False): if q is False: return False @@ -113,21 +55,6 @@ def handler(q=False): for evt in request['data']: - ''' - print(" DATA ") - print(request['data']) - - reportlab_generator. - - report = ReportGenerator() - report.report_headers() - report.from_event(evt) - report.asciidoc() - - print(" REPORT : ") - print(report) - ''' - misp_event = MISPEvent() misp_event.load(evt) @@ -165,8 +92,3 @@ def introspection(): def version(): moduleinfo['config'] = moduleconfig return moduleinfo - -import pprint - -if __name__ == "__main__": - pprint.pprint("test") \ No newline at end of file From 9f0f6e71e87f658ebb4518f9c9142db1cf0efe1e Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Fri, 22 Feb 2019 12:15:28 +0100 Subject: [PATCH 024/207] chg: PyMISP requirement --- REQUIREMENTS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/REQUIREMENTS b/REQUIREMENTS index 0720e90..6f3d1b2 100644 --- a/REQUIREMENTS +++ b/REQUIREMENTS @@ -3,7 +3,7 @@ -e git+https://github.com/D4-project/BGP-Ranking.git/@7e698f87366e6f99b4d0d11852737db28e3ddc62#egg=pybgpranking&subdirectory=client -e git+https://github.com/D4-project/IPASN-History.git/@e846cd36fe1ed6b22f60890bba89f84e61b62e59#egg=pyipasnhistory&subdirectory=client -e git+https://github.com/MISP/PyIntel471.git@0df8d51f1c1425de66714b3a5a45edb69b8cc2fc#egg=pyintel471 --e git+https://github.com/MISP/PyMISP.git@2c877f2aec11b7f5d2f23dfc5ce7398b2ce33b48#egg=pymisp +-e git+https://github.com/MISP/PyMISP.git@ccd7565d3ce4693b96ea2352792099b40c53e494#egg=pymisp -e git+https://github.com/Rafiot/uwhoisd.git@f6f035e52213c8abc20f2084d28cfffb399457cb#egg=uwhois&subdirectory=client -e git+https://github.com/sebdraven/pydnstrails@48c1f740025c51289f43a24863d1845ff12fd21a#egg=pydnstrails -e git+https://github.com/sebdraven/pyonyphe@66329baeee7cab844f2203c047c2551828eaf14d#egg=pyonyphe From 66ee78e7af41f7062e3239e7fe676c80dc8a378d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Sun, 24 Feb 2019 16:02:13 -0800 Subject: [PATCH 025/207] new: Add systemd launcher --- etc/systemd/system/misp-modules.service | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 etc/systemd/system/misp-modules.service diff --git a/etc/systemd/system/misp-modules.service b/etc/systemd/system/misp-modules.service new file mode 100644 index 0000000..3ff05ae --- /dev/null +++ b/etc/systemd/system/misp-modules.service @@ -0,0 +1,14 @@ +[Unit] +Description=System-wide instance of the MISP Modules +After=network.target + +[Service] +User=www-data +Group=www-data +WorkingDirectory=/usr/local/src/misp-modules +Environment="PATH=/var/www/MISP/venv/bin" +ExecStart=misp-modules -l 127.0.0.1 -s + +[Install] +WantedBy=multi-user.target + From 43d2ae6203a484e4614166a610a3d6bc73c12b03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Sun, 24 Feb 2019 18:20:28 -0800 Subject: [PATCH 026/207] fix: systemd service --- etc/systemd/system/misp-modules.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/systemd/system/misp-modules.service b/etc/systemd/system/misp-modules.service index 3ff05ae..99cd102 100644 --- a/etc/systemd/system/misp-modules.service +++ b/etc/systemd/system/misp-modules.service @@ -7,7 +7,7 @@ User=www-data Group=www-data WorkingDirectory=/usr/local/src/misp-modules Environment="PATH=/var/www/MISP/venv/bin" -ExecStart=misp-modules -l 127.0.0.1 -s +ExecStart=/var/www/MISP/venv/bin/misp-modules -l 127.0.0.1 -s [Install] WantedBy=multi-user.target From a3a871f2faa6b27c79e8c55bd0b128b9edbc7cf3 Mon Sep 17 00:00:00 2001 From: Falconieri Date: Mon, 25 Feb 2019 15:51:33 +0100 Subject: [PATCH 027/207] fix [exportpdf] update parameters for links generation --- misp_modules/modules/export_mod/pdfexport.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/misp_modules/modules/export_mod/pdfexport.py b/misp_modules/modules/export_mod/pdfexport.py index ef3d775..977ee87 100755 --- a/misp_modules/modules/export_mod/pdfexport.py +++ b/misp_modules/modules/export_mod/pdfexport.py @@ -18,7 +18,9 @@ moduleinfo = {'version': '2', 'module-type': ['export'], 'require_standard_format': True} -moduleconfig = [] +# config fields that your code expects from the site admin +moduleconfig = ["MISP_base_url_for_dynamic_link", "MISP_name_for_metadata"] + mispattributes = {} outputFileExtension = "pdf" @@ -53,12 +55,19 @@ def handler(q=False): if 'data' not in request: return False + config = {} + + # Construct config object for reportlab_generator + for config_item in moduleconfig : + if (request.get('config')) and (request['config'].get(config_item) is not None): + config[config_item] = request['config'].get(config_item) + for evt in request['data']: misp_event = MISPEvent() misp_event.load(evt) - pdf = reportlab_generator.get_base64_from_value(reportlab_generator.convert_event_in_pdf_buffer(misp_event)) + pdf = reportlab_generator.get_base64_from_value(reportlab_generator.convert_event_in_pdf_buffer(misp_event, config)) return {'response': [], 'data': str(pdf, 'utf-8')} From 0d8ead483e204045eaff9af35bc61836488c30fc Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Mon, 25 Feb 2019 16:18:41 +0100 Subject: [PATCH 028/207] chg: [PyMISP] dep updated to the latest version --- REQUIREMENTS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/REQUIREMENTS b/REQUIREMENTS index 6f3d1b2..e42481b 100644 --- a/REQUIREMENTS +++ b/REQUIREMENTS @@ -3,7 +3,7 @@ -e git+https://github.com/D4-project/BGP-Ranking.git/@7e698f87366e6f99b4d0d11852737db28e3ddc62#egg=pybgpranking&subdirectory=client -e git+https://github.com/D4-project/IPASN-History.git/@e846cd36fe1ed6b22f60890bba89f84e61b62e59#egg=pyipasnhistory&subdirectory=client -e git+https://github.com/MISP/PyIntel471.git@0df8d51f1c1425de66714b3a5a45edb69b8cc2fc#egg=pyintel471 --e git+https://github.com/MISP/PyMISP.git@ccd7565d3ce4693b96ea2352792099b40c53e494#egg=pymisp +-e git+https://github.com/MISP/PyMISP.git@345f055844fed0acdfb34c52d96d1751728bb82c#egg=pymisp -e git+https://github.com/Rafiot/uwhoisd.git@f6f035e52213c8abc20f2084d28cfffb399457cb#egg=uwhois&subdirectory=client -e git+https://github.com/sebdraven/pydnstrails@48c1f740025c51289f43a24863d1845ff12fd21a#egg=pydnstrails -e git+https://github.com/sebdraven/pyonyphe@66329baeee7cab844f2203c047c2551828eaf14d#egg=pyonyphe From 9e48b3994a70cc9447c279470a3dce9b23a1d278 Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Mon, 25 Feb 2019 20:29:04 +0100 Subject: [PATCH 029/207] chg: [requirements] updated --- REQUIREMENTS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/REQUIREMENTS b/REQUIREMENTS index e42481b..69b0568 100644 --- a/REQUIREMENTS +++ b/REQUIREMENTS @@ -3,7 +3,7 @@ -e git+https://github.com/D4-project/BGP-Ranking.git/@7e698f87366e6f99b4d0d11852737db28e3ddc62#egg=pybgpranking&subdirectory=client -e git+https://github.com/D4-project/IPASN-History.git/@e846cd36fe1ed6b22f60890bba89f84e61b62e59#egg=pyipasnhistory&subdirectory=client -e git+https://github.com/MISP/PyIntel471.git@0df8d51f1c1425de66714b3a5a45edb69b8cc2fc#egg=pyintel471 --e git+https://github.com/MISP/PyMISP.git@345f055844fed0acdfb34c52d96d1751728bb82c#egg=pymisp +-e git+https://github.com/MISP/PyMISP.git@634ecc3ac308d01ebf5f5fbb9aace7746a2b8707#egg=pymisp -e git+https://github.com/Rafiot/uwhoisd.git@f6f035e52213c8abc20f2084d28cfffb399457cb#egg=uwhois&subdirectory=client -e git+https://github.com/sebdraven/pydnstrails@48c1f740025c51289f43a24863d1845ff12fd21a#egg=pydnstrails -e git+https://github.com/sebdraven/pyonyphe@66329baeee7cab844f2203c047c2551828eaf14d#egg=pyonyphe From bbe7fe51e70ee7ef24fb9a9573d7ad5a85ddaf9f Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Mon, 25 Feb 2019 20:34:48 +0100 Subject: [PATCH 030/207] chg: [pipenv] Pipfile.lock updated --- Pipfile.lock | 174 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 106 insertions(+), 68 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 19f32f0..1c08572 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -150,9 +150,9 @@ }, "httplib2": { "hashes": [ - "sha256:f61fb838a94ce3b349aa32c92fd8430f7e3511afdb18bf9640d647e30c90a6d6" + "sha256:4ba6b8fd77d0038769bf3c33c9a96a6f752bc4cdf739701fdcaf210121f399d4" ], - "version": "==0.12.0" + "version": "==0.12.1" }, "idna": { "hashes": [ @@ -177,10 +177,10 @@ }, "jsonschema": { "hashes": [ - "sha256:000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08", - "sha256:6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02" + "sha256:acc8a90c31d11060516cfd0b414b9f8bcf4bc691b21f0f786ea57dd5255c79db", + "sha256:dd3f8ecb1b52d94d45eedb67cb86cac57b94ded562c5d98f63719e55ce58557b" ], - "version": "==2.6.0" + "version": "==3.0.0" }, "maclookup": { "hashes": [ @@ -281,22 +281,22 @@ }, "psutil": { "hashes": [ - "sha256:04d2071100aaad59f9bcbb801be2125d53b2e03b1517d9fed90b45eea51d297e", - "sha256:1aba93430050270750d046a179c5f3d6e1f5f8b96c20399ba38c596b28fc4d37", - "sha256:3ac48568f5b85fee44cd8002a15a7733deca056a191d313dbf24c11519c0c4a8", - "sha256:96f3fdb4ef7467854d46ad5a7e28eb4c6dc6d455d751ddf9640cd6d52bdb03d7", - "sha256:b755be689d6fc8ebc401e1d5ce5bac867e35788f10229e166338484eead51b12", - "sha256:c8ee08ad1b716911c86f12dc753eb1879006224fd51509f077987bb6493be615", - "sha256:d0c4230d60376aee0757d934020b14899f6020cd70ef8d2cb4f228b6ffc43e8f", - "sha256:d23f7025bac9b3e38adc6bd032cdaac648ac0074d18e36950a04af35458342e8", - "sha256:f0fcb7d3006dd4d9ccf3ccd0595d44c6abbfd433ec31b6ca177300ee3f19e54e" + "sha256:5ce6b5eb0267233459f4d3980c205828482f450999b8f5b684d9629fea98782a", + "sha256:72cebfaa422b7978a1d3632b65ff734a34c6b34f4578b68a5c204d633756b810", + "sha256:77c231b4dff8c1c329a4cd1c22b96c8976c597017ff5b09993cd148d6a94500c", + "sha256:8846ab0be0cdccd6cc92ecd1246a16e2f2e49f53bd73e522c3a75ac291e1b51d", + "sha256:a013b4250ccbddc9d22feca0f986a1afc71717ad026c0f2109bbffd007351191", + "sha256:ad43b83119eeea6d5751023298cd331637e542cbd332196464799e25a5519f8f", + "sha256:c177777c787d247d02dae6c855330f9ed3e1abf8ca1744c26dd5ff968949999a", + "sha256:ec1ef313530a9457e48d25e3fdb1723dfa636008bf1b970027462d46f2555d59", + "sha256:ef3e5e02b3c5d1df366abe7b4820400d5c427579668ad4465ff189d28ded5ebd" ], - "version": "==5.5.0" + "version": "==5.5.1" }, "pybgpranking": { "editable": true, "git": "https://github.com/D4-project/BGP-Ranking.git/", - "ref": "7e698f87366e6f99b4d0d11852737db28e3ddc62", + "ref": "37c97ae252ec4bf1d67733a49d4895c8cb009cf9", "subdirectory": "client" }, "pydnstrails": { @@ -333,12 +333,12 @@ "pymisp": { "editable": true, "git": "https://github.com/MISP/PyMISP.git", - "ref": "2c877f2aec11b7f5d2f23dfc5ce7398b2ce33b48" + "ref": "634ecc3ac308d01ebf5f5fbb9aace7746a2b8707" }, "pyonyphe": { "editable": true, "git": "https://github.com/sebdraven/pyonyphe", - "ref": "66329baeee7cab844f2203c047c2551828eaf14d" + "ref": "cbb0168d5cb28a9f71f7ab3773164a7039ccdb12" }, "pyparsing": { "hashes": [ @@ -361,6 +361,12 @@ "index": "pypi", "version": "==2.1" }, + "pyrsistent": { + "hashes": [ + "sha256:3ca82748918eb65e2d89f222b702277099aca77e34843c5eb9d52451173970e2" + ], + "version": "==0.14.11" + }, "pytesseract": { "hashes": [ "sha256:11c20321595b6e2e904b594633edf1a717212b13bac7512986a2d807b8849770" @@ -370,10 +376,10 @@ }, "python-dateutil": { "hashes": [ - "sha256:063df5763652e21de43de7d9e00ccf239f953a832941e37be541614732cdfc93", - "sha256:88f9287c0174266bb0d8cedd395cfba9c58e87e5ad86b2ce58859bc11be3cf02" + "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", + "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" ], - "version": "==2.7.5" + "version": "==2.8.0" }, "pyyaml": { "hashes": [ @@ -400,10 +406,43 @@ }, "redis": { "hashes": [ - "sha256:74c892041cba46078ae1ef845241548baa3bd3634f9a6f0f952f006eb1619c71", - "sha256:7ba8612bbfd966dea8c62322543fed0095da2834dbd5a7c124afbc617a156aa7" + "sha256:724932360d48e5407e8f82e405ab3650a36ed02c7e460d1e6fddf0f038422b54", + "sha256:9b19425a38fd074eb5795ff2b0d9a55b46a44f91f5347995f27e3ad257a7d775" ], - "version": "==3.1.0" + "version": "==3.2.0" + }, + "reportlab": { + "hashes": [ + "sha256:069f684cd0aaa518a27dc9124aed29cee8998e21ddf19604e53214ec8462bdd7", + "sha256:09b68ec01d86b4b120456b3f3202570ec96f57624e3a4fc36f3829323391daa4", + "sha256:0c32be9a406172c29ea20ff55a709ccac1e7fb09f15aba67cb7b455fd1d3dbe0", + "sha256:233196cf25e97cfe7c452524ea29d9a4909f1cb66599299233be1efaaaa7a7a3", + "sha256:2b5e4533f3e5b962835a5ce44467e66d1ecc822761d1b508077b5087a06be338", + "sha256:2e860bcdace5a558356802a92ae8658d7e5fdaa00ded82e83a3f2987c562cb66", + "sha256:3546029e63a9a9dc24ee38959eb417678c2425b96cd27b31e09e216dafc94666", + "sha256:4452b93f9c73b6b70311e7d69082d64da81b38e91bfb4766397630092e6da6fd", + "sha256:528c74a1c6527d1859c2c7a64a94a1cba485b00175162ea23699ae58a1e94939", + "sha256:6116e750f98018febc08dfee6df20446cf954adbcfa378d2c703d56c8864aff3", + "sha256:6b2b3580c647d75ef129172cb3da648cdb24566987b0b59c5ebb80ab770748d6", + "sha256:727b5f2bed08552d143fc99649b1863c773729f580a416844f9d9967bb0a1ae8", + "sha256:74c24a3ec0a3d4f8acb13a07192f45bdb54a1cc3c2286241677e7e8bcd5011fa", + "sha256:98ccd2f8b4f8636db05f3f14db0b471ad6bb4b66ae0dc9052c4822b3bd5d6a7d", + "sha256:a5905aa567946bc938b489a7249c7890c3fd3c9b7b5680dece5bc551c2ddbe0d", + "sha256:acbb7f676b8586b770719e9683eda951fdb38eb7970d46fcbf3cdda88d912a64", + "sha256:b5e30f865add48cf880f1c363eb505b97f2f7baaa88c155f87a335a76515a3e5", + "sha256:be2a7c33a2c28bbd3f453ffe4f0e5200b88c803a097f4cf52d69c6b53fad7a8f", + "sha256:c356bb600f59ac64955813d6497a08bfd5d0c451cb5829b61e3913d0ac084e26", + "sha256:c7ec4ae2393beab584921b1287a04e94fd98c28315e348362d89b85f4b464546", + "sha256:d476edc831bb3e9ebd04d1403abaf3ea57b3e4c2276c91a54fdfb6efbd3f9d97", + "sha256:db059e1a0691c872784062421ec51848539eb4f5210142682e61059a5ca7cc55", + "sha256:dd423a6753509ab14a0ac1b5be39d219c8f8d3781cce3deb4f45eda31969b5e8", + "sha256:ed9b7c0d71ce6fe2b31c6cde530ad8238632b876a5d599218739bda142a77f7c", + "sha256:f0a2465af4006f97b05e1f1546d67d3a3213d414894bf28be7f87f550a7f4a55", + "sha256:f20bfe26e57e8e1f575a9e0325be04dd3562db9f247ffdd73b5d4df6dec53bc2", + "sha256:f3463f2cb40a1b515ac0133ba859eca58f53b56760da9abb27ed684c565f853c", + "sha256:facc3c9748ab1525fb8401a1223bce4f24f0d6aa1a9db86c55db75777ccf40f9" + ], + "version": "==3.5.13" }, "requests": { "hashes": [ @@ -422,10 +461,10 @@ }, "shodan": { "hashes": [ - "sha256:c40abb6ff2fd66bdee9f773746fb961eefdfaa8e720a07cb12fb70def136268d" + "sha256:f93b7199e89eecf5c84647f66316c2c044c3aebfc1fe4d9caa43dfda07f74c4e" ], "index": "pypi", - "version": "==1.10.4" + "version": "==1.11.1" }, "sigmatools": { "hashes": [ @@ -443,10 +482,10 @@ }, "soupsieve": { "hashes": [ - "sha256:466910df7561796a60748826781ebe9a888f7a1668a636ae86783f44d10aae73", - "sha256:87db12ae79194f0ff9808d2b1641c4f031ae39ffa3cab6b907ea7c1e5e5ed445" + "sha256:afa56bf14907bb09403e5d15fbed6275caa4174d36b975226e3b67a3bb6e2c4b", + "sha256:eaed742b48b1f3e2d45ba6f79401b2ed5dc33b2123dfe216adb90d4bfa0ade26" ], - "version": "==1.7.3" + "version": "==1.8" }, "sparqlwrapper": { "hashes": [ @@ -500,49 +539,48 @@ "uwhois": { "editable": true, "git": "https://github.com/Rafiot/uwhoisd.git", - "ref": "f6f035e52213c8abc20f2084d28cfffb399457cb", + "ref": "411572840eba4c72dc321c549b36a54ed5cea9de", "subdirectory": "client" }, "vulners": { "hashes": [ - "sha256:5f05404041cfaa8e5367bf884fc9ee319ebf34bedc495d7f84c433fa121cdb49", - "sha256:919b24df64ea55b6a8ba13e2a0530578f8a4be6a9cee257bf2214046e81c6f35", - "sha256:d45ecb13f5111947056a2dcc071b3e3fd45f6ad654eda06526245bba3850325e" + "sha256:40041bcf893fa1bfaf29c650369d9a249991911f28b4d8795f7bc06508013e14", + "sha256:6d00709300dcc7e2727499d8a60f51eaced1dc6b63cc19cb8a4b065b658c51aa", + "sha256:de8cef247c9852c39bd54434e63026b46bdb2bd4ca22813bf66626b7d359b0f3" ], "index": "pypi", - "version": "==1.4.0" + "version": "==1.4.4" }, "wand": { "hashes": [ - "sha256:3e59e4bda9ef9d643d90e881cc950c8eee1508ec2cde1c150a1cbd5a12c1c007", - "sha256:52763dbf65d00cf98d7bc910b49329eea15896249c5555d47e169f2b6efbe166" + "sha256:7d6b8dc9d4eaccc430b9c86e6b749013220c994970a3f39e902b397e2fa732c3", + "sha256:cc0b5c9cd50fecd10dc8888b739dd5984c6f8085d2954f34903b83ca39a91236" ], "index": "pypi", - "version": "==0.5.0" + "version": "==0.5.1" }, "xlsxwriter": { "hashes": [ - "sha256:7cc07619760641b67112dbe0df938399d4d915d9b9924bb58eb5c17384d29cc6", - "sha256:ae22658a0fc5b9e875fa97c213d1ffd617d86dc49bf08be99ebdac814db7bf36" + "sha256:de9ef46088489915eaaee00c7088cff93cf613e9990b46b933c98eb46f21b47f", + "sha256:df96eafc3136d9e790e35d6725b473e46ada6f585c1f6519da69b27f5c8873f7" ], - "version": "==1.1.2" + "version": "==1.1.5" }, "yara-python": { "hashes": [ - "sha256:03e5c5e333c8572e7994b0b11964d515d61a393f23c5e272f8d0e4229f368c58", - "sha256:0423e08bd618752a028ac0405ff8e0103f3a8fd607dde7618a64a4c010c3757b", - "sha256:0a0dd632dcdb347d1a9a8b1f6a83b3a77d5e63f691357ea4021fb1cf1d7ff0a4", - "sha256:728b99627a8072a877eaaa4dafb4eff39d1b14ff4fd70d39f18899ce81e29625", - "sha256:7cb0d5724eccfa52e1bcd352a56cb4dc422aa51f5f6d0945d4f830783927513b", - "sha256:8c76531e89806c0309586dd4863a972d12f1d5d63261c6d4b9331a99859fd1d8", - "sha256:9472676583e212bc4e17c2236634e02273d53c872b350f0571b48e06183de233", - "sha256:9735b680a7d95c1d3f255c351bb067edc62cdb3c0999f7064278cb2c85245405", - "sha256:997f104590167220a9af5564c042ec4d6534261e7b8a5b49655d8dffecc6b8a2", - "sha256:a48e071d02a3699363e628ac899b5b7237803bcb4b512c92ebcb4fb9b1488497", - "sha256:b67c0d75a6519ca357b4b85ede9768c96a81fff20fbc169bd805ff009ddee561" + "sha256:0d002170b2f2c56ff75c846ad1e6765f59d4569e81494c76f15243197e4a974c", + "sha256:16be7c7623685b4b2813db33a39553d6faef236ddffa0758c08e2071ab11ed84", + "sha256:2031ac6ac01754dbc82b5a47b69cb91302c6b66ea9d9f2f27cc2eaf771e19c14", + "sha256:228a96efc86c766d968c984bd80f5ebb0bb775afb9045c10fb632e2b7275c9c1", + "sha256:468a9770e6b578f0562a540b6cb5cafd4122bea989404b53440d4eb065d54eda", + "sha256:752d12a795159b806cd74ab7f0fd7c3a14cb6e17c9e4a818511dc7a4932b15df", + "sha256:755406cb5fa944d5e0dd097a4b25c3fcdd5ba244f0367114afed1ba30ccd2a12", + "sha256:7936c10c8802fc279802dcdda8270d3fda5c3d3c8fbe6bb02010934ed30b8929", + "sha256:95c8d39ee5938744dbd8e0153ec6d466f8a4ed11b8ac7b1068f498c26a292b65", + "sha256:cfd00cfb7bcbe862b0793f91b5393bad3fb37da78883af19924059367ba80f51" ], "index": "pypi", - "version": "==3.8.1" + "version": "==3.9.0" }, "yarl": { "hashes": [ @@ -643,11 +681,11 @@ }, "flake8": { "hashes": [ - "sha256:09b9bb539920776da542e67a570a5df96ff933c9a08b62cfae920bcc789e4383", - "sha256:e0f8cd519cfc0072c0ee31add5def09d2b3ef6040b34dc426445c3af9b02163c" + "sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661", + "sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8" ], "index": "pypi", - "version": "==3.7.4" + "version": "==3.7.7" }, "idna": { "hashes": [ @@ -665,11 +703,11 @@ }, "more-itertools": { "hashes": [ - "sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4", - "sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc", - "sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9" + "sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40", + "sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1" ], - "version": "==5.0.0" + "markers": "python_version > '2.7'", + "version": "==6.0.0" }, "nose": { "hashes": [ @@ -682,17 +720,17 @@ }, "pluggy": { "hashes": [ - "sha256:8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616", - "sha256:980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a" + "sha256:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f", + "sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746" ], - "version": "==0.8.1" + "version": "==0.9.0" }, "py": { "hashes": [ - "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", - "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" + "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", + "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" ], - "version": "==1.7.0" + "version": "==1.8.0" }, "pycodestyle": { "hashes": [ @@ -710,11 +748,11 @@ }, "pytest": { "hashes": [ - "sha256:65aeaa77ae87c7fc95de56285282546cfa9c886dc8e5dc78313db1c25e21bc07", - "sha256:6ac6d467d9f053e95aaacd79f831dbecfe730f419c6c7022cb316b365cd9199d" + "sha256:067a1d4bf827ffdd56ad21bd46674703fce77c5957f6c1eef731f6146bfcef1c", + "sha256:9687049d53695ad45cf5fdc7bbd51f0c49f1ea3ecfc4b7f3fde7501b541f17f4" ], "index": "pypi", - "version": "==4.2.0" + "version": "==4.3.0" }, "requests": { "hashes": [ From 637d7f25381b33f30a947cc23c50325f088e21cc Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Mon, 25 Feb 2019 20:42:45 +0100 Subject: [PATCH 031/207] chg: [requirements] reportlab added --- REQUIREMENTS | 1 + 1 file changed, 1 insertion(+) diff --git a/REQUIREMENTS b/REQUIREMENTS index 69b0568..4891c60 100644 --- a/REQUIREMENTS +++ b/REQUIREMENTS @@ -47,6 +47,7 @@ rdflib==4.2.2 redis==3.1.0 requests-cache==0.4.13 requests==2.21.0 +reportlab shodan==1.10.4 sigmatools==0.7.1 six==1.12.0 From b0ea67e393f91aefbf770123ce6f4cd0699d0e5e Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Mon, 25 Feb 2019 21:11:24 +0100 Subject: [PATCH 032/207] chg: [pipenv] fix the temporary issue that python-yara is not officially released --- Pipfile | 3 ++- Pipfile.lock | 26 ++++++++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/Pipfile b/Pipfile index c086e62..45c05f5 100644 --- a/Pipfile +++ b/Pipfile @@ -25,12 +25,13 @@ pytesseract = "*" pygeoip = "*" beautifulsoup4 = "*" oauth2 = "*" -yara-python = ">=3.8.0" +yara-python = "==3.8.1" sigmatools = "*" stix2-patterns = "*" maclookup = "*" vulners = "*" blockchain = "*" +reportlab = "*" pyintel471 = {editable = true,git = "https://github.com/MISP/PyIntel471.git"} shodan = "*" Pillow = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 1c08572..9e6265d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f501a84bdd41ca21a2af020278ce030985cccd5f2f5683cd075797be4523587d" + "sha256": "d0cd64bfe7702365d3ea66d1f51a1ec8592df2490899e7e163fe38f97172561e" }, "pipfile-spec": 6, "requires": { @@ -442,6 +442,7 @@ "sha256:f3463f2cb40a1b515ac0133ba859eca58f53b56760da9abb27ed684c565f853c", "sha256:facc3c9748ab1525fb8401a1223bce4f24f0d6aa1a9db86c55db75777ccf40f9" ], + "index": "pypi", "version": "==3.5.13" }, "requests": { @@ -568,19 +569,20 @@ }, "yara-python": { "hashes": [ - "sha256:0d002170b2f2c56ff75c846ad1e6765f59d4569e81494c76f15243197e4a974c", - "sha256:16be7c7623685b4b2813db33a39553d6faef236ddffa0758c08e2071ab11ed84", - "sha256:2031ac6ac01754dbc82b5a47b69cb91302c6b66ea9d9f2f27cc2eaf771e19c14", - "sha256:228a96efc86c766d968c984bd80f5ebb0bb775afb9045c10fb632e2b7275c9c1", - "sha256:468a9770e6b578f0562a540b6cb5cafd4122bea989404b53440d4eb065d54eda", - "sha256:752d12a795159b806cd74ab7f0fd7c3a14cb6e17c9e4a818511dc7a4932b15df", - "sha256:755406cb5fa944d5e0dd097a4b25c3fcdd5ba244f0367114afed1ba30ccd2a12", - "sha256:7936c10c8802fc279802dcdda8270d3fda5c3d3c8fbe6bb02010934ed30b8929", - "sha256:95c8d39ee5938744dbd8e0153ec6d466f8a4ed11b8ac7b1068f498c26a292b65", - "sha256:cfd00cfb7bcbe862b0793f91b5393bad3fb37da78883af19924059367ba80f51" + "sha256:03e5c5e333c8572e7994b0b11964d515d61a393f23c5e272f8d0e4229f368c58", + "sha256:0423e08bd618752a028ac0405ff8e0103f3a8fd607dde7618a64a4c010c3757b", + "sha256:0a0dd632dcdb347d1a9a8b1f6a83b3a77d5e63f691357ea4021fb1cf1d7ff0a4", + "sha256:728b99627a8072a877eaaa4dafb4eff39d1b14ff4fd70d39f18899ce81e29625", + "sha256:7cb0d5724eccfa52e1bcd352a56cb4dc422aa51f5f6d0945d4f830783927513b", + "sha256:8c76531e89806c0309586dd4863a972d12f1d5d63261c6d4b9331a99859fd1d8", + "sha256:9472676583e212bc4e17c2236634e02273d53c872b350f0571b48e06183de233", + "sha256:9735b680a7d95c1d3f255c351bb067edc62cdb3c0999f7064278cb2c85245405", + "sha256:997f104590167220a9af5564c042ec4d6534261e7b8a5b49655d8dffecc6b8a2", + "sha256:a48e071d02a3699363e628ac899b5b7237803bcb4b512c92ebcb4fb9b1488497", + "sha256:b67c0d75a6519ca357b4b85ede9768c96a81fff20fbc169bd805ff009ddee561" ], "index": "pypi", - "version": "==3.9.0" + "version": "==3.8.1" }, "yarl": { "hashes": [ From e7fd7e8eb20ed92ab5b09e83d7acf004fa366b6f Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Mon, 25 Feb 2019 21:18:26 +0100 Subject: [PATCH 033/207] chg: [pdfexport] make flake8 happy --- misp_modules/modules/export_mod/pdfexport.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/misp_modules/modules/export_mod/pdfexport.py b/misp_modules/modules/export_mod/pdfexport.py index 977ee87..6b0c12f 100755 --- a/misp_modules/modules/export_mod/pdfexport.py +++ b/misp_modules/modules/export_mod/pdfexport.py @@ -1,11 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from datetime import date import json -import shlex -import subprocess -import base64 from pymisp import MISPEvent from pymisp.tools import reportlab_generator @@ -46,6 +42,7 @@ class ReportGenerator(): self.misp_event = MISPEvent() self.misp_event.load(event) + def handler(q=False): if q is False: return False @@ -58,12 +55,11 @@ def handler(q=False): config = {} # Construct config object for reportlab_generator - for config_item in moduleconfig : + for config_item in moduleconfig: if (request.get('config')) and (request['config'].get(config_item) is not None): config[config_item] = request['config'].get(config_item) for evt in request['data']: - misp_event = MISPEvent() misp_event.load(evt) From 2a59c6becc3e24c56febe838adbcc965e929d49b Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Mon, 25 Feb 2019 21:33:47 +0100 Subject: [PATCH 034/207] chg: [doc] PDF export --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ee4f2f8..501e54f 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ * [CEF](misp_modules/modules/export_mod/cef_export.py) module to export Common Event Format (CEF). * [GoAML export](misp_modules/modules/export_mod/goamlexport.py) module to export in [GoAML format](http://goaml.unodc.org/goaml/en/index.html). * [Lite Export](misp_modules/modules/export_mod/liteexport.py) module to export a lite event. -* [Simple PDF export](misp_modules/modules/export_mod/pdfexport.py) module to export in PDF (required: asciidoctor-pdf). +* [PDF export](misp_modules/modules/export_mod/pdfexport.py) module to export an event in PDF. * [Nexthink query format](misp_modules/modules/export_mod/nexthinkexport.py) module to export in Nexthink query format. * [osquery](misp_modules/modules/export_mod/osqueryexport.py) module to export in [osquery](https://osquery.io/) query format. * [ThreatConnect](misp_modules/modules/export_mod/threat_connect_export.py) module to export in ThreatConnect CSV format. From a770bfc5934157f895f211539911f6dcd7481e64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Tue, 26 Feb 2019 16:43:08 -0800 Subject: [PATCH 035/207] chg: Bump dependencies, add update script --- Pipfile.lock | 2 +- REQUIREMENTS | 32 ++++++++++++++++---------------- setup.py | 1 + tools/update_misp_modules.sh | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 17 deletions(-) create mode 100755 tools/update_misp_modules.sh diff --git a/Pipfile.lock b/Pipfile.lock index 9e6265d..85cd9db 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -333,7 +333,7 @@ "pymisp": { "editable": true, "git": "https://github.com/MISP/PyMISP.git", - "ref": "634ecc3ac308d01ebf5f5fbb9aace7746a2b8707" + "ref": "62e047f3c1972e21aa36a8882bebf4488cdc1f84" }, "pyonyphe": { "editable": true, diff --git a/REQUIREMENTS b/REQUIREMENTS index 4891c60..4709747 100644 --- a/REQUIREMENTS +++ b/REQUIREMENTS @@ -1,17 +1,16 @@ -i https://pypi.org/simple -e . --e git+https://github.com/D4-project/BGP-Ranking.git/@7e698f87366e6f99b4d0d11852737db28e3ddc62#egg=pybgpranking&subdirectory=client +-e git+https://github.com/D4-project/BGP-Ranking.git/@37c97ae252ec4bf1d67733a49d4895c8cb009cf9#egg=pybgpranking&subdirectory=client -e git+https://github.com/D4-project/IPASN-History.git/@e846cd36fe1ed6b22f60890bba89f84e61b62e59#egg=pyipasnhistory&subdirectory=client -e git+https://github.com/MISP/PyIntel471.git@0df8d51f1c1425de66714b3a5a45edb69b8cc2fc#egg=pyintel471 --e git+https://github.com/MISP/PyMISP.git@634ecc3ac308d01ebf5f5fbb9aace7746a2b8707#egg=pymisp --e git+https://github.com/Rafiot/uwhoisd.git@f6f035e52213c8abc20f2084d28cfffb399457cb#egg=uwhois&subdirectory=client +-e git+https://github.com/MISP/PyMISP.git@62e047f3c1972e21aa36a8882bebf4488cdc1f84#egg=pymisp +-e git+https://github.com/Rafiot/uwhoisd.git@411572840eba4c72dc321c549b36a54ed5cea9de#egg=uwhois&subdirectory=client -e git+https://github.com/sebdraven/pydnstrails@48c1f740025c51289f43a24863d1845ff12fd21a#egg=pydnstrails --e git+https://github.com/sebdraven/pyonyphe@66329baeee7cab844f2203c047c2551828eaf14d#egg=pyonyphe +-e git+https://github.com/sebdraven/pyonyphe@cbb0168d5cb28a9f71f7ab3773164a7039ccdb12#egg=pyonyphe aiohttp==3.4.4 antlr4-python3-runtime==4.7.2 ; python_version >= '3' async-timeout==3.0.1 attrs==18.2.0 -backscatter==0.2.3 beautifulsoup4==4.7.1 blockchain==1.4.4 certifi==2018.11.29 @@ -24,42 +23,43 @@ domaintools-api==0.3.3 enum-compat==0.0.2 ez-setup==0.9 future==0.17.1 -httplib2==0.12.0 +httplib2==0.12.1 idna-ssl==1.1.0 ; python_version < '3.7' idna==2.8 isodate==0.6.0 -jsonschema==2.6.0 +jsonschema==3.0.0 maclookup==1.0.3 multidict==4.5.2 oauth2==1.9.0.post1 passivetotal==1.0.30 pillow==5.4.1 -psutil==5.5.0 +psutil==5.5.1 pyeupi==1.0 pygeoip==0.3.2 pyparsing==2.3.1 pypdns==1.3 pypssl==2.1 +pyrsistent==0.14.11 pytesseract==0.2.6 -python-dateutil==2.7.5 +python-dateutil==2.8.0 pyyaml==3.13 rdflib==4.2.2 -redis==3.1.0 +redis==3.2.0 +reportlab==3.5.13 requests-cache==0.4.13 requests==2.21.0 -reportlab -shodan==1.10.4 +shodan==1.11.1 sigmatools==0.7.1 six==1.12.0 -soupsieve==1.7.3 +soupsieve==1.8 sparqlwrapper==1.8.2 stix2-patterns==1.1.0 tornado==5.1.1 url-normalize==1.4.1 urlarchiver==0.2 urllib3==1.24.1 -vulners==1.4.0 -wand==0.5.0 -xlsxwriter==1.1.2 +vulners==1.4.4 +wand==0.5.1 +xlsxwriter==1.1.5 yara-python==3.8.1 yarl==1.3.0 diff --git a/setup.py b/setup.py index fc78750..55ed8b7 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,7 @@ setup( description='MISP modules are autonomous modules that can be used for expansion and other services in MISP', packages=find_packages(), entry_points={'console_scripts': ['misp-modules = misp_modules:main']}, + scripts=['tools/update_misp_modules.sh'], test_suite="tests", classifiers=[ 'License :: OSI Approved :: GNU Affero General Public License v3', diff --git a/tools/update_misp_modules.sh b/tools/update_misp_modules.sh new file mode 100755 index 0000000..e0578fd --- /dev/null +++ b/tools/update_misp_modules.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +set -e +set -x + +# Updates the MISP Modules while respecting the current permissions +# It aims to support the two following installation methods: +# * Everything is runinng on the same machine following the MISP installation guide. +# * The modules are installed using pipenv on a different machine from the one where MISP is running. + +if [ -d "/var/www/MISP" ] && [ -d "/usr/local/src/misp-modules" ] +then + echo "MISP is installed on the same machine, following the recommanded install script. Using MISP virtualenv." + PATH_TO_MISP="/var/www/MISP" + PATH_TO_MISP_MODULES="/usr/local/src/misp-modules" + + pushd ${PATH_TO_MISP_MODULES} + USER=`stat -c "%U" .` + sudo -H -u ${USER} git pull + sudo -H -u ${USER} ${PATH_TO_MISP}/venv/bin/pip install -U -r REQUIREMENTS + sudo -H -u ${USER} ${PATH_TO_MISP}/venv/bin/pip install -U -e . + + popd +else + if ! [ -x "$(command -v pipenv)" ]; then + echo 'Error: pipenv not available, unable to automatically update.' >&2 + exit 1 + fi + + echo "Standalone mode, use pipenv from the current directory." + git pull + pipenv install +fi + + From 75953c32a7c093401a88e0f05d2447c9f4dbbac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Tue, 26 Feb 2019 16:48:11 -0800 Subject: [PATCH 036/207] chr: Restart the modules after update --- tools/update_misp_modules.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/update_misp_modules.sh b/tools/update_misp_modules.sh index e0578fd..372d146 100755 --- a/tools/update_misp_modules.sh +++ b/tools/update_misp_modules.sh @@ -20,6 +20,8 @@ then sudo -H -u ${USER} ${PATH_TO_MISP}/venv/bin/pip install -U -r REQUIREMENTS sudo -H -u ${USER} ${PATH_TO_MISP}/venv/bin/pip install -U -e . + service misp-modules restart + popd else if ! [ -x "$(command -v pipenv)" ]; then From a937b7c85dae6a22f7f3d5373e628e9f36aae384 Mon Sep 17 00:00:00 2001 From: Falconieri Date: Wed, 27 Feb 2019 12:45:22 +0100 Subject: [PATCH 037/207] fix: [reportlab] Textual description parameter --- misp_modules/modules/export_mod/pdfexport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/export_mod/pdfexport.py b/misp_modules/modules/export_mod/pdfexport.py index 6b0c12f..c143b5e 100755 --- a/misp_modules/modules/export_mod/pdfexport.py +++ b/misp_modules/modules/export_mod/pdfexport.py @@ -15,7 +15,7 @@ moduleinfo = {'version': '2', 'require_standard_format': True} # config fields that your code expects from the site admin -moduleconfig = ["MISP_base_url_for_dynamic_link", "MISP_name_for_metadata"] +moduleconfig = ["MISP_base_url_for_dynamic_link", "MISP_name_for_metadata", "Activate_textual_description"] mispattributes = {} From a2716bc05d0c94a4c4d147591c2cb74a1a685882 Mon Sep 17 00:00:00 2001 From: Falconieri Date: Fri, 1 Mar 2019 09:11:34 +0100 Subject: [PATCH 038/207] fix: [exportpdf] add configmodule parameter for galaxy --- misp_modules/modules/export_mod/pdfexport.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/misp_modules/modules/export_mod/pdfexport.py b/misp_modules/modules/export_mod/pdfexport.py index c143b5e..096992b 100755 --- a/misp_modules/modules/export_mod/pdfexport.py +++ b/misp_modules/modules/export_mod/pdfexport.py @@ -15,8 +15,8 @@ moduleinfo = {'version': '2', 'require_standard_format': True} # config fields that your code expects from the site admin -moduleconfig = ["MISP_base_url_for_dynamic_link", "MISP_name_for_metadata", "Activate_textual_description"] - +moduleconfig = ["MISP_base_url_for_dynamic_link", "MISP_name_for_metadata", "Activate_textual_description", + "Activate_galaxy_description"] mispattributes = {} outputFileExtension = "pdf" From aef8dbbe2eca411757bc3e56c6dd4040de0daec5 Mon Sep 17 00:00:00 2001 From: Falconieri Date: Fri, 1 Mar 2019 09:17:38 +0100 Subject: [PATCH 039/207] fix: [exportpdf] problem on one line --- misp_modules/modules/export_mod/pdfexport.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/misp_modules/modules/export_mod/pdfexport.py b/misp_modules/modules/export_mod/pdfexport.py index 096992b..d4e3be5 100755 --- a/misp_modules/modules/export_mod/pdfexport.py +++ b/misp_modules/modules/export_mod/pdfexport.py @@ -15,8 +15,7 @@ moduleinfo = {'version': '2', 'require_standard_format': True} # config fields that your code expects from the site admin -moduleconfig = ["MISP_base_url_for_dynamic_link", "MISP_name_for_metadata", "Activate_textual_description", - "Activate_galaxy_description"] +moduleconfig = ["MISP_base_url_for_dynamic_link", "MISP_name_for_metadata", "Activate_textual_description","Activate_galaxy_description"] mispattributes = {} outputFileExtension = "pdf" From 7d7c90143ef062cbb97e4f2baf88ea5e32632033 Mon Sep 17 00:00:00 2001 From: Falconieri Date: Fri, 1 Mar 2019 09:25:02 +0100 Subject: [PATCH 040/207] fix: [exportpdf] mising whitespace --- misp_modules/modules/export_mod/pdfexport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/export_mod/pdfexport.py b/misp_modules/modules/export_mod/pdfexport.py index d4e3be5..7402e27 100755 --- a/misp_modules/modules/export_mod/pdfexport.py +++ b/misp_modules/modules/export_mod/pdfexport.py @@ -15,7 +15,7 @@ moduleinfo = {'version': '2', 'require_standard_format': True} # config fields that your code expects from the site admin -moduleconfig = ["MISP_base_url_for_dynamic_link", "MISP_name_for_metadata", "Activate_textual_description","Activate_galaxy_description"] +moduleconfig = ["MISP_base_url_for_dynamic_link", "MISP_name_for_metadata", "Activate_textual_description", "Activate_galaxy_description"] mispattributes = {} outputFileExtension = "pdf" From 3b415cb53a2eff78dc1d3d56264764a81de6c3a9 Mon Sep 17 00:00:00 2001 From: cgi1 Date: Fri, 1 Mar 2019 12:13:27 +0100 Subject: [PATCH 041/207] Adding virtualenv to apt-get install --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 501e54f..6ef4bf4 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ ## How to install and start MISP modules in a Python virtualenv? ~~~~bash -sudo apt-get install python3-dev python3-pip libpq5 libjpeg-dev tesseract-ocr imagemagick +sudo apt-get install python3-dev python3-pip libpq5 libjpeg-dev tesseract-ocr imagemagick virtualenv sudo -u www-data virtualenv -p python3 /var/www/MISP/venv cd /usr/local/src/ sudo git clone https://github.com/MISP/misp-modules.git From a30bcc5dd20f44c19d867e5cb9328113ad8ea980 Mon Sep 17 00:00:00 2001 From: Falconieri Date: Mon, 4 Mar 2019 12:36:18 +0100 Subject: [PATCH 042/207] fix: [exportpdf] add parameters --- misp_modules/modules/export_mod/pdfexport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/export_mod/pdfexport.py b/misp_modules/modules/export_mod/pdfexport.py index 7402e27..d12dece 100755 --- a/misp_modules/modules/export_mod/pdfexport.py +++ b/misp_modules/modules/export_mod/pdfexport.py @@ -15,7 +15,7 @@ moduleinfo = {'version': '2', 'require_standard_format': True} # config fields that your code expects from the site admin -moduleconfig = ["MISP_base_url_for_dynamic_link", "MISP_name_for_metadata", "Activate_textual_description", "Activate_galaxy_description"] +moduleconfig = ["MISP_base_url_for_dynamic_link", "MISP_name_for_metadata", "Activate_textual_description", "Activate_galaxy_description", "Activate_related_events", "Activate_internationalization_fonts"] mispattributes = {} outputFileExtension = "pdf" From e3ddbe66a62830f69a47c228209dcef6f3a6c14e Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Mon, 4 Mar 2019 23:08:58 +0100 Subject: [PATCH 043/207] chg: [doc] asciidoctor requirement removed (new PDF module use reportlab) --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index 6ef4bf4..951de64 100644 --- a/README.md +++ b/README.md @@ -94,8 +94,6 @@ sudo git clone https://github.com/MISP/misp-modules.git cd misp-modules sudo -u www-data /var/www/MISP/venv/bin/pip install -I -r REQUIREMENTS sudo -u www-data /var/www/MISP/venv/bin/pip install . -sudo apt install ruby-pygments.rb -y -sudo gem install asciidoctor-pdf --pre sudo sed -i -e '$i \sudo -u www-data /var/www/MISP/venv/bin/misp-modules -l 127.0.0.1 -s > /tmp/misp-modules_rc.local.log &\n' /etc/rc.local /var/www/MISP/venv/bin/misp-modules -l 127.0.0.1 -s & #to start the modules ~~~~ @@ -109,8 +107,6 @@ sudo git clone https://github.com/MISP/misp-modules.git cd misp-modules sudo pip3 install -I -r REQUIREMENTS sudo pip3 install -I . -sudo apt install ruby-pygments.rb -y -sudo gem install asciidoctor-pdf --pre sudo sed -i -e '$i \sudo -u www-data /var/www/MISP/venv/bin/misp-modules -l 127.0.0.1 -s > /tmp/misp-modules_rc.local.log &\n' /etc/rc.local /var/www/MISP/venv/bin/misp-modules -l 127.0.0.1 -s & #to start the modules ~~~~ @@ -125,7 +121,6 @@ cd misp-modules scl enable rh-python36 ‘python3 –m pip install cryptography’ scl enable rh-python36 ‘python3 –m pip install -I -r REQUIREMENTS’ scl enable rh-python36 ‘python3 –m pip install –I .’ -scl enable rh-ruby22 ‘gem install asciidoctor-pdf –pre’ ~~~~ Create the service file /etc/systemd/system/misp-workers.service : ~~~~ From 32e10ee27333410a1ed4d2c4afe10d8c3b7456be Mon Sep 17 00:00:00 2001 From: Falconieri Date: Tue, 5 Mar 2019 10:39:07 +0100 Subject: [PATCH 044/207] fix: [exportpdf] custom path parameter --- misp_modules/modules/export_mod/pdfexport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/export_mod/pdfexport.py b/misp_modules/modules/export_mod/pdfexport.py index d12dece..44b3bc9 100755 --- a/misp_modules/modules/export_mod/pdfexport.py +++ b/misp_modules/modules/export_mod/pdfexport.py @@ -15,7 +15,7 @@ moduleinfo = {'version': '2', 'require_standard_format': True} # config fields that your code expects from the site admin -moduleconfig = ["MISP_base_url_for_dynamic_link", "MISP_name_for_metadata", "Activate_textual_description", "Activate_galaxy_description", "Activate_related_events", "Activate_internationalization_fonts"] +moduleconfig = ["MISP_base_url_for_dynamic_link", "MISP_name_for_metadata", "Activate_textual_description", "Activate_galaxy_description", "Activate_related_events", "Activate_internationalization_fonts", "Custom_fonts_path"] mispattributes = {} outputFileExtension = "pdf" From 30c08708c62a773d24de1b36308cdb613d0f55a5 Mon Sep 17 00:00:00 2001 From: Falconieri Date: Tue, 5 Mar 2019 12:11:44 +0100 Subject: [PATCH 045/207] fix: [exportpdf] update documentation --- doc/export_mod/pdfexport.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/export_mod/pdfexport.json b/doc/export_mod/pdfexport.json index 9803c77..f1654dc 100644 --- a/doc/export_mod/pdfexport.json +++ b/doc/export_mod/pdfexport.json @@ -1,7 +1,7 @@ { "description": "Simple export of a MISP event to PDF.", - "requirements": ["PyMISP", "asciidoctor"], - "features": "The module takes care of the PDF file building, and work with any MISP Event. Except the requirement of asciidoctor, used to create the file, there is no special feature concerning the Event.", + "requirements": ["PyMISP", "reportlab"], + "features": "The module takes care of the PDF file building, and work with any MISP Event. Except the requirement of reportlab, used to create the file, there is no special feature concerning the Event. Some parameters can be given through the config dict. 'MISP_base_url_for_dynamic_link' is your MISP URL, to attach an hyperlink to your event on your MISP instance from the PDF. Keep it clear to avoid hyperlinks in the generated pdf.\n 'MISP_name_for_metadata' is your CERT or MISP instance name. Used as text in the PDF' metadata\n 'Activate_textual_description' is a boolean (True or void) to activate the textual description/header abstract of an event\n 'Activate_galaxy_description' is a boolean (True or void) to activate the description of event related galaxies.\n 'Activate_related_events' is a boolean (True or void) to activate the description of related event. Be aware this might leak information on confidential events linked to the current event !\n 'Activate_internationalization_fonts' is a boolean (True or void) to activate Noto fonts instead of default fonts (Helvetica). This allows the support of CJK alphabet. Be sure to have followed the procedure to download Noto fonts (~70Mo) in the right place (/tools/pdf_fonts/Noto_TTF), to allow PyMisp to find and use them during PDF generation.\n 'Custom_fonts_path' is a text (path or void) to the TTF file of your choice, to create the PDF with it. Be aware the PDF won't support bold/italic/special style anymore with this option ", "references": ["https://acrobat.adobe.com/us/en/acrobat/about-adobe-pdf.html"], "input": "MISP Event", "output": "MISP Event in a PDF file." From 9611c7f2a9a1bd84081ca3bc625f731d8185c842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Sat, 9 Mar 2019 06:15:16 +0100 Subject: [PATCH 046/207] chg: Bump Requirements --- Pipfile.lock | 74 ++++++++++++++++++++++++++-------------------------- REQUIREMENTS | 14 +++++----- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 85cd9db..36d7c3c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -59,10 +59,10 @@ }, "attrs": { "hashes": [ - "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", - "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" + "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", + "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" ], - "version": "==18.2.0" + "version": "==19.1.0" }, "beautifulsoup4": { "hashes": [ @@ -177,10 +177,10 @@ }, "jsonschema": { "hashes": [ - "sha256:acc8a90c31d11060516cfd0b414b9f8bcf4bc691b21f0f786ea57dd5255c79db", - "sha256:dd3f8ecb1b52d94d45eedb67cb86cac57b94ded562c5d98f63719e55ce58557b" + "sha256:0c0a81564f181de3212efa2d17de1910f8732fa1b71c42266d983cd74304e20d", + "sha256:a5f6559964a3851f59040d3b961de5e68e70971afb88ba519d27e6a039efff1a" ], - "version": "==3.0.0" + "version": "==3.0.1" }, "maclookup": { "hashes": [ @@ -281,17 +281,17 @@ }, "psutil": { "hashes": [ - "sha256:5ce6b5eb0267233459f4d3980c205828482f450999b8f5b684d9629fea98782a", - "sha256:72cebfaa422b7978a1d3632b65ff734a34c6b34f4578b68a5c204d633756b810", - "sha256:77c231b4dff8c1c329a4cd1c22b96c8976c597017ff5b09993cd148d6a94500c", - "sha256:8846ab0be0cdccd6cc92ecd1246a16e2f2e49f53bd73e522c3a75ac291e1b51d", - "sha256:a013b4250ccbddc9d22feca0f986a1afc71717ad026c0f2109bbffd007351191", - "sha256:ad43b83119eeea6d5751023298cd331637e542cbd332196464799e25a5519f8f", - "sha256:c177777c787d247d02dae6c855330f9ed3e1abf8ca1744c26dd5ff968949999a", - "sha256:ec1ef313530a9457e48d25e3fdb1723dfa636008bf1b970027462d46f2555d59", - "sha256:ef3e5e02b3c5d1df366abe7b4820400d5c427579668ad4465ff189d28ded5ebd" + "sha256:1020a37214c4138e34962881372b40f390582b5c8245680c04349c2afb785a25", + "sha256:151c9858c268a1523e16fab33e3bc3bae8a0e57b57cf7fcad85fb409cbac6baf", + "sha256:1c8e6444ca1cee9a60a1a35913b8409722f7474616e0e21004e4ffadba59964b", + "sha256:722dc0dcce5272f3c5c41609fdc2c8f0ee3f976550c2d2f2057e26ba760be9c0", + "sha256:86f61a1438c026c980a4c3e2dd88a5774a3a0f00d6d0954d6c5cf8d1921b804e", + "sha256:c4a2f42abee709ed97b4498c21aa608ac31fc1f7cc8aa60ebdcd3c80757a038d", + "sha256:d9cdc2e82aeb82200fff3640f375fac39d88b1bed27ce08377cd7fb0e3621cb7", + "sha256:da6676a484adec2fdd3e1ce1b70799881ffcb958e40208dd4c5beba0011f3589", + "sha256:dca71c08335fbfc6929438fe3a502f169ba96dd20e50b3544053d6be5cb19d82" ], - "version": "==5.5.1" + "version": "==5.6.0" }, "pybgpranking": { "editable": true, @@ -333,7 +333,7 @@ "pymisp": { "editable": true, "git": "https://github.com/MISP/PyMISP.git", - "ref": "62e047f3c1972e21aa36a8882bebf4488cdc1f84" + "ref": "b8759673b91e733c307698abdc0d5ed82fd7e0de" }, "pyonyphe": { "editable": true, @@ -469,10 +469,10 @@ }, "sigmatools": { "hashes": [ - "sha256:98c9897f27e7c99f398bff537bb6b0259599177d955f8b60a22db1b246f9cb0b" + "sha256:3bdbd2ee99c32f245e948d6b882219729ab379685dd7366e4d6149c390e08170" ], "index": "pypi", - "version": "==0.7.1" + "version": "==0.9" }, "six": { "hashes": [ @@ -506,15 +506,15 @@ }, "tornado": { "hashes": [ - "sha256:0662d28b1ca9f67108c7e3b77afabfb9c7e87bde174fbda78186ecedc2499a9d", - "sha256:4e5158d97583502a7e2739951553cbd88a72076f152b4b11b64b9a10c4c49409", - "sha256:732e836008c708de2e89a31cb2fa6c0e5a70cb60492bee6f1ea1047500feaf7f", - "sha256:8154ec22c450df4e06b35f131adc4f2f3a12ec85981a203301d310abf580500f", - "sha256:8e9d728c4579682e837c92fdd98036bd5cdefa1da2aaf6acf26947e6dd0c01c5", - "sha256:d4b3e5329f572f055b587efc57d29bd051589fb5a43ec8898c77a47ec2fa2bbb", - "sha256:e5f2585afccbff22390cddac29849df463b252b711aa2ce7c5f3f342a5b3b444" + "sha256:1a58f2d603476d5e462f7c28ca1dbb5ac7e51348b27a9cac849cdec3471101f8", + "sha256:33f93243cd46dd398e5d2bbdd75539564d1f13f25d704cfc7541db74066d6695", + "sha256:34e59401afcecf0381a28228daad8ed3275bcb726810654612d5e9c001f421b7", + "sha256:35817031611d2c296c69e5023ea1f9b5720be803e3bb119464bb2a0405d5cd70", + "sha256:666b335cef5cc2759c21b7394cff881f71559aaf7cb8c4458af5bb6cb7275b47", + "sha256:81203efb26debaaef7158187af45bc440796de9fb1df12a75b65fae11600a255", + "sha256:de274c65f45f6656c375cdf1759dbf0bc52902a1e999d12a35eb13020a641a53" ], - "version": "==5.1.1" + "version": "==6.0.1" }, "url-normalize": { "hashes": [ @@ -545,12 +545,12 @@ }, "vulners": { "hashes": [ - "sha256:40041bcf893fa1bfaf29c650369d9a249991911f28b4d8795f7bc06508013e14", - "sha256:6d00709300dcc7e2727499d8a60f51eaced1dc6b63cc19cb8a4b065b658c51aa", - "sha256:de8cef247c9852c39bd54434e63026b46bdb2bd4ca22813bf66626b7d359b0f3" + "sha256:08a7ccb2b210d45143354c6161c73fe209dc14fae8692e8b793b36b79330ad11", + "sha256:bfe2478cc11c69ba7e436d7a5df925e227565782c0bd603929fb3d612c73d78d", + "sha256:d035f6a883625878a1dc377830d17d9702ef138ca31569ac01cb8686874f89cd" ], "index": "pypi", - "version": "==1.4.4" + "version": "==1.4.5" }, "wand": { "hashes": [ @@ -611,10 +611,10 @@ }, "attrs": { "hashes": [ - "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", - "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" + "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", + "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" ], - "version": "==18.2.0" + "version": "==19.1.0" }, "certifi": { "hashes": [ @@ -743,10 +743,10 @@ }, "pyflakes": { "hashes": [ - "sha256:5e8c00e30c464c99e0b501dc160b13a14af7f27d4dffb529c556e30a159e231d", - "sha256:f277f9ca3e55de669fba45b7393a1449009cff5a37d1af10ebb76c52765269cd" + "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", + "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" ], - "version": "==2.1.0" + "version": "==2.1.1" }, "pytest": { "hashes": [ diff --git a/REQUIREMENTS b/REQUIREMENTS index 4709747..d672411 100644 --- a/REQUIREMENTS +++ b/REQUIREMENTS @@ -3,14 +3,14 @@ -e git+https://github.com/D4-project/BGP-Ranking.git/@37c97ae252ec4bf1d67733a49d4895c8cb009cf9#egg=pybgpranking&subdirectory=client -e git+https://github.com/D4-project/IPASN-History.git/@e846cd36fe1ed6b22f60890bba89f84e61b62e59#egg=pyipasnhistory&subdirectory=client -e git+https://github.com/MISP/PyIntel471.git@0df8d51f1c1425de66714b3a5a45edb69b8cc2fc#egg=pyintel471 --e git+https://github.com/MISP/PyMISP.git@62e047f3c1972e21aa36a8882bebf4488cdc1f84#egg=pymisp +-e git+https://github.com/MISP/PyMISP.git@b8759673b91e733c307698abdc0d5ed82fd7e0de#egg=pymisp -e git+https://github.com/Rafiot/uwhoisd.git@411572840eba4c72dc321c549b36a54ed5cea9de#egg=uwhois&subdirectory=client -e git+https://github.com/sebdraven/pydnstrails@48c1f740025c51289f43a24863d1845ff12fd21a#egg=pydnstrails -e git+https://github.com/sebdraven/pyonyphe@cbb0168d5cb28a9f71f7ab3773164a7039ccdb12#egg=pyonyphe aiohttp==3.4.4 antlr4-python3-runtime==4.7.2 ; python_version >= '3' async-timeout==3.0.1 -attrs==18.2.0 +attrs==19.1.0 beautifulsoup4==4.7.1 blockchain==1.4.4 certifi==2018.11.29 @@ -27,13 +27,13 @@ httplib2==0.12.1 idna-ssl==1.1.0 ; python_version < '3.7' idna==2.8 isodate==0.6.0 -jsonschema==3.0.0 +jsonschema==3.0.1 maclookup==1.0.3 multidict==4.5.2 oauth2==1.9.0.post1 passivetotal==1.0.30 pillow==5.4.1 -psutil==5.5.1 +psutil==5.6.0 pyeupi==1.0 pygeoip==0.3.2 pyparsing==2.3.1 @@ -49,16 +49,16 @@ reportlab==3.5.13 requests-cache==0.4.13 requests==2.21.0 shodan==1.11.1 -sigmatools==0.7.1 +sigmatools==0.9 six==1.12.0 soupsieve==1.8 sparqlwrapper==1.8.2 stix2-patterns==1.1.0 -tornado==5.1.1 +tornado==6.0.1 url-normalize==1.4.1 urlarchiver==0.2 urllib3==1.24.1 -vulners==1.4.4 +vulners==1.4.5 wand==0.5.1 xlsxwriter==1.1.5 yara-python==3.8.1 From c4ced9dfbf49ec605f230b35564e17a94bcea6d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Sat, 9 Mar 2019 06:40:23 +0100 Subject: [PATCH 047/207] fix: Tornado expects a KILL now. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b574d4c..e8fea8e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,14 +19,14 @@ script: - pid=$! - sleep 5 - pipenv run nosetests --with-coverage --cover-package=misp_modules - - kill -s INT $pid + - kill -s KILL $pid - pushd ~/ - pipenv run coverage run -m --parallel-mode --source=misp_modules misp_modules.__init__ -s -l 127.0.0.1 & - pid=$! - popd - sleep 5 - pipenv run nosetests --with-coverage --cover-package=misp_modules - - kill -s INT $pid + - kill -s KILL $pid - pipenv run flake8 --ignore=E501,W503 misp_modules after_success: From 4b77cb5055e0a114c5bf361cadefc4aacbe0ae39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Sun, 10 Mar 2019 21:17:30 +0100 Subject: [PATCH 048/207] new: Add missing dependency (backscatter) --- Pipfile | 1 + Pipfile.lock | 92 ++++++++++++++++++++++++++++++---------------------- REQUIREMENTS | 4 ++- 3 files changed, 57 insertions(+), 40 deletions(-) diff --git a/Pipfile b/Pipfile index 45c05f5..2f2d172 100644 --- a/Pipfile +++ b/Pipfile @@ -41,6 +41,7 @@ domaintools_api = "*" misp-modules = {editable = true,path = "."} pybgpranking = {editable = true,git = "https://github.com/D4-project/BGP-Ranking.git/",subdirectory = "client"} pyipasnhistory = {editable = true,git = "https://github.com/D4-project/IPASN-History.git/",subdirectory = "client"} +backscatter = "*" [requires] python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock index 36d7c3c..3c902e7 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d0cd64bfe7702365d3ea66d1f51a1ec8592df2490899e7e163fe38f97172561e" + "sha256": "23dec0fa6400c828e294ea9981b433903c17358ca61d7abdaec8df5a1c89f08c" }, "pipfile-spec": 6, "requires": { @@ -64,6 +64,14 @@ ], "version": "==19.1.0" }, + "backscatter": { + "hashes": [ + "sha256:7a0d1aa3661635de81e2a09b15d53e35cbe399a111cc58a70925f80e6874abd3", + "sha256:afb0efcf5d2551dac953ec4c38fb710b274b8e811775650e02c1ef42cafb14c8" + ], + "index": "pypi", + "version": "==0.2.4" + }, "beautifulsoup4": { "hashes": [ "sha256:034740f6cb549b4e932ae1ab975581e6103ac8f942200a0e9759065984391858", @@ -82,10 +90,10 @@ }, "certifi": { "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", + "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae" ], - "version": "==2018.11.29" + "version": "==2019.3.9" }, "chardet": { "hashes": [ @@ -504,6 +512,12 @@ "index": "pypi", "version": "==1.1.0" }, + "tabulate": { + "hashes": [ + "sha256:8af07a39377cee1103a5c8b3330a421c2d99b9141e9cc5ddd2e3263fea416943" + ], + "version": "==0.8.3" + }, "tornado": { "hashes": [ "sha256:1a58f2d603476d5e462f7c28ca1dbb5ac7e51348b27a9cac849cdec3471101f8", @@ -618,10 +632,10 @@ }, "certifi": { "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", + "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae" ], - "version": "==2018.11.29" + "version": "==2019.3.9" }, "chardet": { "hashes": [ @@ -640,39 +654,39 @@ }, "coverage": { "hashes": [ - "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f", - "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe", - "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d", - "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0", - "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607", - "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d", - "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b", - "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3", - "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e", - "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815", - "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36", - "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1", - "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14", - "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c", - "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794", - "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b", - "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840", - "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd", - "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82", - "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952", - "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389", - "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f", - "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4", - "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da", - "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647", - "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d", - "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42", - "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478", - "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b", - "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb", - "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9" + "sha256:3684fabf6b87a369017756b551cef29e505cb155ddb892a7a29277b978da88b9", + "sha256:39e088da9b284f1bd17c750ac672103779f7954ce6125fd4382134ac8d152d74", + "sha256:3c205bc11cc4fcc57b761c2da73b9b72a59f8d5ca89979afb0c1c6f9e53c7390", + "sha256:465ce53a8c0f3a7950dfb836438442f833cf6663d407f37d8c52fe7b6e56d7e8", + "sha256:48020e343fc40f72a442c8a1334284620f81295256a6b6ca6d8aa1350c763bbe", + "sha256:5296fc86ab612ec12394565c500b412a43b328b3907c0d14358950d06fd83baf", + "sha256:5f61bed2f7d9b6a9ab935150a6b23d7f84b8055524e7be7715b6513f3328138e", + "sha256:68a43a9f9f83693ce0414d17e019daee7ab3f7113a70c79a3dd4c2f704e4d741", + "sha256:6b8033d47fe22506856fe450470ccb1d8ba1ffb8463494a15cfc96392a288c09", + "sha256:7ad7536066b28863e5835e8cfeaa794b7fe352d99a8cded9f43d1161be8e9fbd", + "sha256:7bacb89ccf4bedb30b277e96e4cc68cd1369ca6841bde7b005191b54d3dd1034", + "sha256:839dc7c36501254e14331bcb98b27002aa415e4af7ea039d9009409b9d2d5420", + "sha256:8f9a95b66969cdea53ec992ecea5406c5bd99c9221f539bca1e8406b200ae98c", + "sha256:932c03d2d565f75961ba1d3cec41ddde00e162c5b46d03f7423edcb807734eab", + "sha256:988529edadc49039d205e0aa6ce049c5ccda4acb2d6c3c5c550c17e8c02c05ba", + "sha256:998d7e73548fe395eeb294495a04d38942edb66d1fa61eb70418871bc621227e", + "sha256:9de60893fb447d1e797f6bf08fdf0dbcda0c1e34c1b06c92bd3a363c0ea8c609", + "sha256:9e80d45d0c7fcee54e22771db7f1b0b126fb4a6c0a2e5afa72f66827207ff2f2", + "sha256:a545a3dfe5082dc8e8c3eb7f8a2cf4f2870902ff1860bd99b6198cfd1f9d1f49", + "sha256:a5d8f29e5ec661143621a8f4de51adfb300d7a476224156a39a392254f70687b", + "sha256:aca06bfba4759bbdb09bf52ebb15ae20268ee1f6747417837926fae990ebc41d", + "sha256:bb23b7a6fd666e551a3094ab896a57809e010059540ad20acbeec03a154224ce", + "sha256:bfd1d0ae7e292105f29d7deaa9d8f2916ed8553ab9d5f39ec65bcf5deadff3f9", + "sha256:c62ca0a38958f541a73cf86acdab020c2091631c137bd359c4f5bddde7b75fd4", + "sha256:c709d8bda72cf4cd348ccec2a4881f2c5848fd72903c185f363d361b2737f773", + "sha256:c968a6aa7e0b56ecbd28531ddf439c2ec103610d3e2bf3b75b813304f8cb7723", + "sha256:df785d8cb80539d0b55fd47183264b7002077859028dfe3070cf6359bf8b2d9c", + "sha256:f406628ca51e0ae90ae76ea8398677a921b36f0bd71aab2099dfed08abd0322f", + "sha256:f46087bbd95ebae244a0eda01a618aff11ec7a069b15a3ef8f6b520db523dcf1", + "sha256:f8019c5279eb32360ca03e9fac40a12667715546eed5c5eb59eb381f2f501260", + "sha256:fc5f4d209733750afd2714e9109816a29500718b32dd9a5db01c0cb3a019b96a" ], - "version": "==4.5.2" + "version": "==4.5.3" }, "entrypoints": { "hashes": [ diff --git a/REQUIREMENTS b/REQUIREMENTS index d672411..99e1c02 100644 --- a/REQUIREMENTS +++ b/REQUIREMENTS @@ -11,9 +11,10 @@ aiohttp==3.4.4 antlr4-python3-runtime==4.7.2 ; python_version >= '3' async-timeout==3.0.1 attrs==19.1.0 +backscatter==0.2.4 beautifulsoup4==4.7.1 blockchain==1.4.4 -certifi==2018.11.29 +certifi==2019.3.9 chardet==3.0.4 click-plugins==1.0.4 click==7.0 @@ -54,6 +55,7 @@ six==1.12.0 soupsieve==1.8 sparqlwrapper==1.8.2 stix2-patterns==1.1.0 +tabulate==0.8.3 tornado==6.0.1 url-normalize==1.4.1 urlarchiver==0.2 From 9c8ee1f3d761987b263a9c68602e24b8f6385916 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Wed, 13 Mar 2019 09:57:28 +0100 Subject: [PATCH 049/207] new: Expansion module to query urlhaus API - Using the next version of modules, taking a MISP attribute as input and able to return attributes and objects - Work still in process in the core part --- misp_modules/modules/expansion/urlhaus.py | 136 ++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 misp_modules/modules/expansion/urlhaus.py diff --git a/misp_modules/modules/expansion/urlhaus.py b/misp_modules/modules/expansion/urlhaus.py new file mode 100644 index 0000000..1a2c80b --- /dev/null +++ b/misp_modules/modules/expansion/urlhaus.py @@ -0,0 +1,136 @@ +from collections import defaultdict +from pymisp import MISPAttribute, MISPObject +import json +import requests + +misperrors = {'error': 'Error'} +mispattributes = {'input': ['domain', 'hostname', 'ip-src', 'ip-dst', 'md5', 'sha256', 'url'], + 'output': ['url', 'filename', 'md5', 'sha256'], + 'format': 'misp_standard'} +moduleinfo = {'version': '0.1', 'author': 'Christian Studer', + 'description': 'Query of the URLhaus API to get additional information about some attributes.', + 'module-type': ['expansion', 'hover']} +moduleconfig = [] + + +def _create_file_object(file_attributes): + return _create_object(file_attributes, 'file') + + +def _create_object(attributes, name): + misp_object = MISPObject(name) + for relation, attribute in attributes.items(): + misp_object.add_attribute(relation, **attribute) + return [misp_object] + + +def _create_objects_with_relationship(file_attributes, vt_attributes): + vt_object = _create_vt_object(vt_attributes)[0] + vt_uuid = vt_object.uuid + file_object = _create_file_object(file_attributes)[0] + file_object.add_reference(vt_uuid, 'analysed-with') + return [file_object, vt_object] + + +def _create_url_attribute(value): + attribute = MISPAttribute() + attribute.from_dict(type='url', value=value) + return attribute + + +def _create_vt_object(vt_attributes): + return _create_object(vt_attributes, 'virustotal_report') + + +def _handle_payload_urls(response): + filenames = [] + urls = [] + if response: + for url in response: + urls.append(url['url']) + if url['filename']: + filenames.append(url['filename']) + return filenames, urls + + +def _query_host_api(attribute): + response = requests.post('https://urlhaus-api.abuse.ch/v1/host/', data={'host': attribute['value']}).json() + attributes = [] + if 'urls' in response and response['urls']: + for url in response['urls']: + attributes.append(_create_url_attribute(url['url']).to_json()) + return {'results': {'Attribute': attributes}} + + +def _query_payload_api(attribute): + hash_type = attribute['type'] + response = requests.post('https://urlhaus-api.abuse.ch/v1/payload/', data={'{}_hash'.format(hash_type): attribute['value']}).json() + results = defaultdict(list) + filenames, urls = _handle_payload_urls(response['urls']) + other_hash_type = 'md5' if hash_type == 'sha256' else 'sha256' + file_object = MISPObject('file') + if attribute['object_id'] != '0': + file_object.id = attribute['object_id'] + for key, relation in zip(('{}_hash'.format(other_hash_type), 'file_size'), (other_hash_type, 'size-in-bytes')): + if response[key]: + file_object.add_attribute(relation, **{'type': relation, 'value': response[key]}) + for filename in filenames: + file_object.add_attribute('filename', **{'type': 'filename', 'value': filename}) + for url in urls: + attribute = _create_url_attribute(url) + results['Attribute'].append(attribute.to_json()) + file_object.add_reference(attribute.uuid, 'retrieved-from') + results['Object'].append(file_object.to_json()) + return {'results': results} + + +def _query_url_api(attribute): + response = requests.post('https://urlhaus-api.abuse.ch/v1/url/', data={'url': attribute['value']}).json() + results = defaultdict(list) + if 'payloads' in response and response['payloads']: + objects_mapping = {1: _create_file_object, 2: _create_vt_object, 3: _create_objects_with_relationship} + file_keys = ('filename', 'response_size', 'response_md5', 'response_sha256') + file_relations = ('filename', 'size-in-bytes', 'md5', 'sha256') + vt_keys = ('result', 'link') + vt_types = ('text', 'link') + vt_relations = ('detection-ratio', 'permalink') + for payload in response['payloads']: + args = [] + object_score = 0 + file_attributes = {relation: {'type': relation, 'value': payload[key]} for key, relation in zip(file_keys, file_relations) if payload[key]} + if file_attributes: + object_score += 1 + args.append(file_attributes) + if payload['virustotal']: + virustotal = payload['virustotal'] + vt_attributes = {relation: {'type': vt_type, 'value': virustotal[key]} for key, vt_type, relation in zip(vt_keys, vt_types, vt_relations)} + if vt_attributes: + object_score += 2 + args.append(vt_attributes) + try: + results['Object'].extend([misp_object.to_json() for misp_object in objects_mapping[object_score](*args)]) + except KeyError: + continue + return {'results': results} + + +_misp_type_mapping = {'url': _query_url_api, 'md5': _query_payload_api, 'sha256': _query_payload_api, + 'domain': _query_host_api, 'hostname': _query_host_api, + 'ip-src': _query_host_api, 'ip-dst': _query_host_api} + + +def handler(q=False): + if q is False: + return False + request = json.loads(q) + attribute = request['attribute'] + return _misp_type_mapping[attribute['type']](attribute) + + +def introspection(): + return mispattributes + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo From 62bc45e03a2c0e64c18a1125691b8873b986b21d Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Thu, 14 Mar 2019 14:31:38 +0100 Subject: [PATCH 050/207] fix: Using to_dict on attributes & objects instead of to_json to make json_decode happy in the core part --- misp_modules/modules/expansion/urlhaus.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/misp_modules/modules/expansion/urlhaus.py b/misp_modules/modules/expansion/urlhaus.py index 1a2c80b..17ea1ea 100644 --- a/misp_modules/modules/expansion/urlhaus.py +++ b/misp_modules/modules/expansion/urlhaus.py @@ -58,7 +58,7 @@ def _query_host_api(attribute): attributes = [] if 'urls' in response and response['urls']: for url in response['urls']: - attributes.append(_create_url_attribute(url['url']).to_json()) + attributes.append(_create_url_attribute(url['url']).to_dict()) return {'results': {'Attribute': attributes}} @@ -78,9 +78,9 @@ def _query_payload_api(attribute): file_object.add_attribute('filename', **{'type': 'filename', 'value': filename}) for url in urls: attribute = _create_url_attribute(url) - results['Attribute'].append(attribute.to_json()) + results['Attribute'].append(attribute.to_dict()) file_object.add_reference(attribute.uuid, 'retrieved-from') - results['Object'].append(file_object.to_json()) + results['Object'].append(file_object.to_dict()) return {'results': results} @@ -108,7 +108,7 @@ def _query_url_api(attribute): object_score += 2 args.append(vt_attributes) try: - results['Object'].extend([misp_object.to_json() for misp_object in objects_mapping[object_score](*args)]) + results['Object'].extend([misp_object.to_dict() for misp_object in objects_mapping[object_score](*args)]) except KeyError: continue return {'results': results} From eb2dcca12b853ead10856b7de15b4d0fff4209f2 Mon Sep 17 00:00:00 2001 From: Sascha Rommelfangen Date: Thu, 14 Mar 2019 14:39:58 +0100 Subject: [PATCH 051/207] fixed a bug when checking malformed BTC addresses --- misp_modules/modules/expansion/btc_steroids.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/misp_modules/modules/expansion/btc_steroids.py b/misp_modules/modules/expansion/btc_steroids.py index 7011eda..5958502 100755 --- a/misp_modules/modules/expansion/btc_steroids.py +++ b/misp_modules/modules/expansion/btc_steroids.py @@ -91,6 +91,7 @@ def mprint(input): def handler(q=False): global result_text global conversion_rates + result_text = "" # start_time = time.time() # now = time.time() if q is False: @@ -105,7 +106,6 @@ def handler(q=False): btc = request['btc'] else: return False - mprint("\nAddress:\t" + btc) try: req = requests.get(blockchain_all.format(btc, "&limit=50")) @@ -113,8 +113,18 @@ def handler(q=False): except Exception: # print(e) print(req.text) - result_text = "" - sys.exit(1) + result_text = "Not a valid BTC address" + r = { + 'results': [ + { + 'types': ['text'], + 'values':[ + str(result_text) + ] + } + ] + } + return r n_tx = jreq['n_tx'] balance = float(jreq['final_balance'] / 100000000) From 97818e17d009f1c20806476aff8d10becb36f30e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D4=9C=D0=B5=D1=95?= <5124946+wesinator@users.noreply.github.com> Date: Thu, 14 Mar 2019 13:28:22 -0400 Subject: [PATCH 052/207] Fix command highlighting --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 951de64..e890433 100644 --- a/README.md +++ b/README.md @@ -505,14 +505,14 @@ sudo git checkout MyModBranch Remove the contents of the build directory and re-install misp-modules. -~~~python +~~~bash sudo rm -fr build/* sudo pip3 install --upgrade . ~~~ SSH in with a different terminal and run `misp-modules` with debugging enabled. -~~~python +~~~bash sudo killall misp-modules misp-modules -d ~~~ From 0b92fd5a5393d7be54878aa0494fedd3f2598ceb Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Thu, 14 Mar 2019 18:48:13 +0100 Subject: [PATCH 053/207] fix: Making json_decode even happier with full json format - Using MISPEvent because it is cleaner & easier - Also cleaner implementation globally --- misp_modules/modules/expansion/urlhaus.py | 188 +++++++++++----------- 1 file changed, 93 insertions(+), 95 deletions(-) diff --git a/misp_modules/modules/expansion/urlhaus.py b/misp_modules/modules/expansion/urlhaus.py index 17ea1ea..b6abd92 100644 --- a/misp_modules/modules/expansion/urlhaus.py +++ b/misp_modules/modules/expansion/urlhaus.py @@ -1,5 +1,5 @@ from collections import defaultdict -from pymisp import MISPAttribute, MISPObject +from pymisp import MISPAttribute, MISPEvent, MISPObject import json import requests @@ -12,111 +12,107 @@ moduleinfo = {'version': '0.1', 'author': 'Christian Studer', 'module-type': ['expansion', 'hover']} moduleconfig = [] - -def _create_file_object(file_attributes): - return _create_object(file_attributes, 'file') +file_keys = ('filename', 'response_size', 'response_md5', 'response_sha256') +file_relations = ('filename', 'size-in-bytes', 'md5', 'sha256') +vt_keys = ('result', 'link') +vt_types = ('text', 'link') +vt_relations = ('detection-ratio', 'permalink') -def _create_object(attributes, name): - misp_object = MISPObject(name) - for relation, attribute in attributes.items(): - misp_object.add_attribute(relation, **attribute) - return [misp_object] +class URLhaus(): + def __init__(self): + super(URLhaus, self).__init__() + self.misp_event = MISPEvent() + + @staticmethod + def _create_vt_object(virustotal): + vt_object = MISPObject('virustotal-report') + for key, vt_type, relation in zip(vt_keys, vt_types, vt_relations): + vt_object.add_attribute(relation, **{'type': vt_type, 'value': virustotal[key]}) + return vt_object + + def get_result(self): + event = json.loads(self.misp_event.to_json())['Event'] + results = {key: event[key] for key in ('Attribute', 'Object') if (key in event and event[key])} + return {'results': results} -def _create_objects_with_relationship(file_attributes, vt_attributes): - vt_object = _create_vt_object(vt_attributes)[0] - vt_uuid = vt_object.uuid - file_object = _create_file_object(file_attributes)[0] - file_object.add_reference(vt_uuid, 'analysed-with') - return [file_object, vt_object] +class HostQuery(URLhaus): + def __init__(self, attribute): + super(HostQuery, self).__init__() + self.attribute = MISPAttribute() + self.attribute.from_dict(**attribute) + self.url = 'https://urlhaus-api.abuse.ch/v1/host/' + + def query_api(self): + response = requests.post(self.url, data={'host': self.attribute.value}).json() + if 'urls' in response and response['urls']: + for url in response['urls']: + self.misp_event.add_attribute(type='url', value=url['url']) -def _create_url_attribute(value): - attribute = MISPAttribute() - attribute.from_dict(type='url', value=value) - return attribute +class PayloadQuery(URLhaus): + def __init__(self, attribute): + super(PayloadQuery, self).__init__() + self.attribute = MISPAttribute() + self.attribute.from_dict(**attribute) + self.url = 'https://urlhaus-api.abuse.ch/v1/payload/' - -def _create_vt_object(vt_attributes): - return _create_object(vt_attributes, 'virustotal_report') - - -def _handle_payload_urls(response): - filenames = [] - urls = [] - if response: - for url in response: - urls.append(url['url']) - if url['filename']: - filenames.append(url['filename']) - return filenames, urls - - -def _query_host_api(attribute): - response = requests.post('https://urlhaus-api.abuse.ch/v1/host/', data={'host': attribute['value']}).json() - attributes = [] - if 'urls' in response and response['urls']: + def query_api(self): + hash_type = self.attribute.type + file_object = MISPObject('file') + if self.attribute.event_id != '0': + file_object.id = self.attribute.event_id + response = requests.post(self.url, data={'{}_hash'.format(hash_type): self.attribute.value}).json() + other_hash_type = 'md5' if hash_type == 'sha256' else 'sha256' + for key, relation in zip(('{}_hash'.format(other_hash_type), 'file_size'), (other_hash_type, 'size-in-bytes')): + if response[key]: + file_object.add_attribute(relation, **{'type': relation, 'value': response[key]}) + if response['virustotal']: + vt_object = self._create_vt_object(response['virustotal']) + file_object.add_reference(vt_object.uuid, 'analyzed-with') + self.misp_event.add_object(**vt_object) + _filename_ = 'filename' for url in response['urls']: - attributes.append(_create_url_attribute(url['url']).to_dict()) - return {'results': {'Attribute': attributes}} + attribute = MISPAttribute() + attribute.from_dict(type='url', value=url['url']) + self.misp_event.add_attribute(**attribute) + file_object.add_reference(attribute.uuid, 'retrieved-from') + if url[_filename_]: + file_object.add_attribute(_filename_, **{'type': _filename_, 'value': url[_filename_]}) + self.misp_event.add_object(**file_object) -def _query_payload_api(attribute): - hash_type = attribute['type'] - response = requests.post('https://urlhaus-api.abuse.ch/v1/payload/', data={'{}_hash'.format(hash_type): attribute['value']}).json() - results = defaultdict(list) - filenames, urls = _handle_payload_urls(response['urls']) - other_hash_type = 'md5' if hash_type == 'sha256' else 'sha256' - file_object = MISPObject('file') - if attribute['object_id'] != '0': - file_object.id = attribute['object_id'] - for key, relation in zip(('{}_hash'.format(other_hash_type), 'file_size'), (other_hash_type, 'size-in-bytes')): - if response[key]: - file_object.add_attribute(relation, **{'type': relation, 'value': response[key]}) - for filename in filenames: - file_object.add_attribute('filename', **{'type': 'filename', 'value': filename}) - for url in urls: - attribute = _create_url_attribute(url) - results['Attribute'].append(attribute.to_dict()) - file_object.add_reference(attribute.uuid, 'retrieved-from') - results['Object'].append(file_object.to_dict()) - return {'results': results} +class UrlQuery(URLhaus): + def __init__(self, attribute): + super(UrlQuery, self).__init__() + self.attribute = MISPAttribute() + self.attribute.from_dict(**attribute) + self.url = 'https://urlhaus-api.abuse.ch/v1/url/' + + @staticmethod + def _create_file_object(payload): + file_object = MISPObject('file') + for key, relation in zip(file_keys, file_relations): + if payload[key]: + file_object.add_attribute(relation, **{'type': relation, 'value': payload[key]}) + return file_object + + def query_api(self): + response = requests.post(self.url, data={'url': self.attribute.value}).json() + if 'payloads' in response and response['payloads']: + for payload in response['payloads']: + file_object = self._create_file_object(payload) + if payload['virustotal']: + vt_object = self._create_vt_object(payload['virustotal']) + file_object.add_reference(vt_object.uuid, 'analyzed-with') + self.misp_event.add_object(**vt_object) + self.misp_event.add_object(**file_object) -def _query_url_api(attribute): - response = requests.post('https://urlhaus-api.abuse.ch/v1/url/', data={'url': attribute['value']}).json() - results = defaultdict(list) - if 'payloads' in response and response['payloads']: - objects_mapping = {1: _create_file_object, 2: _create_vt_object, 3: _create_objects_with_relationship} - file_keys = ('filename', 'response_size', 'response_md5', 'response_sha256') - file_relations = ('filename', 'size-in-bytes', 'md5', 'sha256') - vt_keys = ('result', 'link') - vt_types = ('text', 'link') - vt_relations = ('detection-ratio', 'permalink') - for payload in response['payloads']: - args = [] - object_score = 0 - file_attributes = {relation: {'type': relation, 'value': payload[key]} for key, relation in zip(file_keys, file_relations) if payload[key]} - if file_attributes: - object_score += 1 - args.append(file_attributes) - if payload['virustotal']: - virustotal = payload['virustotal'] - vt_attributes = {relation: {'type': vt_type, 'value': virustotal[key]} for key, vt_type, relation in zip(vt_keys, vt_types, vt_relations)} - if vt_attributes: - object_score += 2 - args.append(vt_attributes) - try: - results['Object'].extend([misp_object.to_dict() for misp_object in objects_mapping[object_score](*args)]) - except KeyError: - continue - return {'results': results} - - -_misp_type_mapping = {'url': _query_url_api, 'md5': _query_payload_api, 'sha256': _query_payload_api, - 'domain': _query_host_api, 'hostname': _query_host_api, - 'ip-src': _query_host_api, 'ip-dst': _query_host_api} +_misp_type_mapping = {'url': UrlQuery, 'md5': PayloadQuery, 'sha256': PayloadQuery, + 'domain': HostQuery, 'hostname': HostQuery, + 'ip-src': HostQuery, 'ip-dst': HostQuery} def handler(q=False): @@ -124,7 +120,9 @@ def handler(q=False): return False request = json.loads(q) attribute = request['attribute'] - return _misp_type_mapping[attribute['type']](attribute) + urlhaus_parser = _misp_type_mapping[attribute['type']](attribute) + urlhaus_parser.query_api() + return urlhaus_parser.get_result() def introspection(): From 1c0984eaec5145643933779601390e811fb9084d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Fri, 15 Mar 2019 11:06:11 +0100 Subject: [PATCH 054/207] fix: Remove unused import --- misp_modules/modules/expansion/btc_steroids.py | 1 - 1 file changed, 1 deletion(-) diff --git a/misp_modules/modules/expansion/btc_steroids.py b/misp_modules/modules/expansion/btc_steroids.py index 2f3bebf..04b7138 100755 --- a/misp_modules/modules/expansion/btc_steroids.py +++ b/misp_modules/modules/expansion/btc_steroids.py @@ -1,4 +1,3 @@ -import sys import json import requests import time From 3e34f38cac78d6e495be8dcf4eee4d4b7dcae181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Fri, 15 Mar 2019 11:12:19 +0100 Subject: [PATCH 055/207] chg: Bump dependencies. --- Pipfile.lock | 56 ++++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 3c902e7..60f1d03 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -289,22 +289,22 @@ }, "psutil": { "hashes": [ - "sha256:1020a37214c4138e34962881372b40f390582b5c8245680c04349c2afb785a25", - "sha256:151c9858c268a1523e16fab33e3bc3bae8a0e57b57cf7fcad85fb409cbac6baf", - "sha256:1c8e6444ca1cee9a60a1a35913b8409722f7474616e0e21004e4ffadba59964b", - "sha256:722dc0dcce5272f3c5c41609fdc2c8f0ee3f976550c2d2f2057e26ba760be9c0", - "sha256:86f61a1438c026c980a4c3e2dd88a5774a3a0f00d6d0954d6c5cf8d1921b804e", - "sha256:c4a2f42abee709ed97b4498c21aa608ac31fc1f7cc8aa60ebdcd3c80757a038d", - "sha256:d9cdc2e82aeb82200fff3640f375fac39d88b1bed27ce08377cd7fb0e3621cb7", - "sha256:da6676a484adec2fdd3e1ce1b70799881ffcb958e40208dd4c5beba0011f3589", - "sha256:dca71c08335fbfc6929438fe3a502f169ba96dd20e50b3544053d6be5cb19d82" + "sha256:23e9cd90db94fbced5151eaaf9033ae9667c033dffe9e709da761c20138d25b6", + "sha256:27858d688a58cbfdd4434e1c40f6c79eb5014b709e725c180488ccdf2f721729", + "sha256:354601a1d1a1322ae5920ba397c58d06c29728a15113598d1a8158647aaa5385", + "sha256:9c3a768486194b4592c7ae9374faa55b37b9877fd9746fb4028cb0ac38fd4c60", + "sha256:c1fd45931889dc1812ba61a517630d126f6185f688eac1693171c6524901b7de", + "sha256:d463a142298112426ebd57351b45c39adb41341b91f033aa903fa4c6f76abecc", + "sha256:e1494d20ffe7891d07d8cb9a8b306c1a38d48b13575265d090fc08910c56d474", + "sha256:ec4b4b638b84d42fc48139f9352f6c6587ee1018d55253542ee28db7480cc653", + "sha256:fa0a570e0a30b9dd618bffbece590ae15726b47f9f1eaf7518dfb35f4d7dcd21" ], - "version": "==5.6.0" + "version": "==5.6.1" }, "pybgpranking": { "editable": true, "git": "https://github.com/D4-project/BGP-Ranking.git/", - "ref": "37c97ae252ec4bf1d67733a49d4895c8cb009cf9", + "ref": "4f2898af7c4e237b6497831d5acf3f4531ac14d8", "subdirectory": "client" }, "pydnstrails": { @@ -335,13 +335,13 @@ "pyipasnhistory": { "editable": true, "git": "https://github.com/D4-project/IPASN-History.git/", - "ref": "e846cd36fe1ed6b22f60890bba89f84e61b62e59", + "ref": "7ef09cf761fc58aa774ea305a33ba75959e39887", "subdirectory": "client" }, "pymisp": { "editable": true, "git": "https://github.com/MISP/PyMISP.git", - "ref": "b8759673b91e733c307698abdc0d5ed82fd7e0de" + "ref": "1dddfd72e08886673e57e23627064f9ea8303d4c" }, "pyonyphe": { "editable": true, @@ -391,19 +391,19 @@ }, "pyyaml": { "hashes": [ - "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", - "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", - "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", - "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", - "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", - "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", - "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", - "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", - "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", - "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", - "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" + "sha256:1adecc22f88d38052fb787d959f003811ca858b799590a5eaa70e63dca50308c", + "sha256:436bc774ecf7c103814098159fbb84c2715d25980175292c648f2da143909f95", + "sha256:460a5a4248763f6f37ea225d19d5c205677d8d525f6a83357ca622ed541830c2", + "sha256:5a22a9c84653debfbf198d02fe592c176ea548cccce47553f35f466e15cf2fd4", + "sha256:7a5d3f26b89d688db27822343dfa25c599627bc92093e788956372285c6298ad", + "sha256:9372b04a02080752d9e6f990179a4ab840227c6e2ce15b95e1278456664cf2ba", + "sha256:a5dcbebee834eaddf3fa7366316b880ff4062e4bcc9787b78c7fbb4a26ff2dd1", + "sha256:aee5bab92a176e7cd034e57f46e9df9a9862a71f8f37cad167c6fc74c65f5b4e", + "sha256:c51f642898c0bacd335fc119da60baae0824f2cde95b0330b56c0553439f0673", + "sha256:c68ea4d3ba1705da1e0d85da6684ac657912679a649e8868bd850d2c299cce13", + "sha256:e23d0cc5299223dcc37885dae624f382297717e459ea24053709675a976a3e19" ], - "version": "==3.13" + "version": "==5.1" }, "rdflib": { "hashes": [ @@ -764,11 +764,11 @@ }, "pytest": { "hashes": [ - "sha256:067a1d4bf827ffdd56ad21bd46674703fce77c5957f6c1eef731f6146bfcef1c", - "sha256:9687049d53695ad45cf5fdc7bbd51f0c49f1ea3ecfc4b7f3fde7501b541f17f4" + "sha256:592eaa2c33fae68c7d75aacf042efc9f77b27c08a6224a4f59beab8d9a420523", + "sha256:ad3ad5c450284819ecde191a654c09b0ec72257a2c711b9633d677c71c9850c4" ], "index": "pypi", - "version": "==4.3.0" + "version": "==4.3.1" }, "requests": { "hashes": [ From 2439d5f75ddeebdf8410586d13c8825449ec6cce Mon Sep 17 00:00:00 2001 From: root Date: Mon, 1 Apr 2019 16:28:19 +0200 Subject: [PATCH 056/207] fix: Fixed object_id variable name typo --- misp_modules/modules/expansion/urlhaus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/urlhaus.py b/misp_modules/modules/expansion/urlhaus.py index b6abd92..dae6fd6 100644 --- a/misp_modules/modules/expansion/urlhaus.py +++ b/misp_modules/modules/expansion/urlhaus.py @@ -62,7 +62,7 @@ class PayloadQuery(URLhaus): hash_type = self.attribute.type file_object = MISPObject('file') if self.attribute.event_id != '0': - file_object.id = self.attribute.event_id + file_object.id = self.attribute.object_id response = requests.post(self.url, data={'{}_hash'.format(hash_type): self.attribute.value}).json() other_hash_type = 'md5' if hash_type == 'sha256' else 'sha256' for key, relation in zip(('{}_hash'.format(other_hash_type), 'file_size'), (other_hash_type, 'size-in-bytes')): From b89d068c041516f020877414bb18b0767fe43d97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Tue, 2 Apr 2019 15:30:11 +0200 Subject: [PATCH 057/207] new: Modules for greynoise, haveibeenpwned and macvendors Source: https://github.com/src7/misp-modules --- misp_modules/modules/expansion/greynoise.py | 40 ++++++++++++++ misp_modules/modules/expansion/hibp.py | 40 ++++++++++++++ misp_modules/modules/expansion/macvendors.py | 42 +++++++++++++++ tests/test_expansions.py | 57 ++++++++++++++++++++ 4 files changed, 179 insertions(+) create mode 100644 misp_modules/modules/expansion/greynoise.py create mode 100644 misp_modules/modules/expansion/hibp.py create mode 100644 misp_modules/modules/expansion/macvendors.py create mode 100644 tests/test_expansions.py diff --git a/misp_modules/modules/expansion/greynoise.py b/misp_modules/modules/expansion/greynoise.py new file mode 100644 index 0000000..324423d --- /dev/null +++ b/misp_modules/modules/expansion/greynoise.py @@ -0,0 +1,40 @@ +import requests +import json + +misperrors = {'error': 'Error'} +mispattributes = {'input': ['ip-dst', 'ip-src'], 'output': ['text']} +moduleinfo = {'version': '0.1', 'author': 'Aurélien Schwab ', 'description': 'Module to access GreyNoise.io API.', 'module-type': ['hover']} +moduleconfig = ['user-agent']#TODO take this into account in the code + +greynoise_api_url = 'http://api.greynoise.io:8888/v1/query/ip' +default_user_agent = 'MISP-Module' + +def handler(q=False): + if q is False: + return False + request = json.loads(q) + for input_type in mispattributes['input']: + if input_type in request: + ip = request[input_type] + break + else: + misperrors['error'] = "Unsupported attributes type" + return misperrors + data = {'ip':ip} + r = requests.post(greynoise_api_url, data=data, headers={'user-agent': default_user_agent})#Real request + if r.status_code == 200:#OK (record found) + response = json.loads(r.text) + if response: + return {'results': [{'types': mispattributes['output'], 'values': response}]} + elif r.status_code == 404:#Not found (not an error) + return {'results': [{'types': mispattributes['output'], 'values': 'No data'}]} + else:#Real error + misperrors['error'] = 'GreyNoise API not accessible (HTTP ' + str(r.status_code) + ')' + return misperrors['error'] + +def introspection(): + return mispattributes + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinf diff --git a/misp_modules/modules/expansion/hibp.py b/misp_modules/modules/expansion/hibp.py new file mode 100644 index 0000000..7010aee --- /dev/null +++ b/misp_modules/modules/expansion/hibp.py @@ -0,0 +1,40 @@ +import requests +import json + +misperrors = {'error': 'Error'} +mispattributes = {'input': ['email-dst', 'email-src'], 'output': ['text']}#All mails as input +moduleinfo = {'version': '0.1', 'author': 'Aurélien Schwab', 'description': 'Module to access haveibeenpwned.com API.', 'module-type': ['hover']} +moduleconfig = ['user-agent']#TODO take this into account in the code + +haveibeenpwned_api_url = 'https://api.haveibeenpwned.com/api/v2/breachedaccount/' +default_user_agent = 'MISP-Module'#User agent (must be set, requiered by API)) + +def handler(q=False): + if q is False: + return False + request = json.loads(q) + for input_type in mispattributes['input']: + if input_type in request: + email = request[input_type] + break + else: + misperrors['error'] = "Unsupported attributes type" + return misperrors + + r = requests.get(haveibeenpwned_api_url + email, headers={'user-agent': default_user_agent})#Real request + if r.status_code == 200:##OK (record found) + breaches = json.loads(r.text) + if breaches: + return {'results': [{'types': mispattributes['output'], 'values': breaches}]} + elif r.status_code == 404:#Not found (not an error) + return {'results': [{'types': mispattributes['output'], 'values': 'OK (Not Found)'}]} + else:#Real error + misperrors['error'] = 'haveibeenpwned.com API not accessible (HTTP ' + str(r.status_code) + ')' + return misperrors['error'] + +def introspection(): + return mispattributes + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinf diff --git a/misp_modules/modules/expansion/macvendors.py b/misp_modules/modules/expansion/macvendors.py new file mode 100644 index 0000000..55d0ef3 --- /dev/null +++ b/misp_modules/modules/expansion/macvendors.py @@ -0,0 +1,42 @@ +import requests +import json + +misperrors = {'error': 'Error'} +mispattributes = {'input': ['mac-address'], 'output': ['text']} +moduleinfo = {'version': '0.1', 'author': 'Aurélien Schwab', 'description': 'Module to access Macvendors API.', 'module-type': ['hover']} +moduleconfig = ['user-agent'] # TODO take this into account in the code + +macvendors_api_url = 'https://api.macvendors.com/' +default_user_agent = 'MISP-Module' + + +def handler(q=False): + if q is False: + return False + request = json.loads(q) + for input_type in mispattributes['input']: + if input_type in request: + mac = request[input_type] + break + else: + misperrors['error'] = "Unsupported attributes type" + return misperrors + r = requests.get(macvendors_api_url + mac, headers={'user-agent': default_user_agent}) # Real request + if r.status_code == 200: # OK (record found) + response = r.text + if response: + return {'results': [{'types': mispattributes['output'], 'values': response}]} + elif r.status_code == 404: # Not found (not an error) + return {'results': [{'types': mispattributes['output'], 'values': 'Not found'}]} + else: # Real error + misperrors['error'] = 'MacVendors API not accessible (HTTP ' + str(r.status_code) + ')' + return misperrors['error'] + + +def introspection(): + return mispattributes + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo diff --git a/tests/test_expansions.py b/tests/test_expansions.py new file mode 100644 index 0000000..d581a31 --- /dev/null +++ b/tests/test_expansions.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import unittest +import requests +from urllib.parse import urljoin + + +class TestExpansions(unittest.TestCase): + + def setUp(self): + self.maxDiff = None + self.headers = {'Content-Type': 'application/json'} + self.url = "http://127.0.0.1:6666/" + + def misp_modules_post(self, query): + return requests.post(urljoin(self.url, "query"), json=query) + + def get_values(self, response): + return response.json()['results'][0]['values'] + + def test_cve(self): + query = {"module": "cve", "vulnerability": "CVE-2010-3333"} + response = self.misp_modules_post(query) + self.assertTrue(self.get_values(response).startswith("Stack-based buffer overflow in Microsoft Office XP SP3, Office 2003 SP3")) + + def test_dns(self): + query = {"module": "dns", "hostname": "www.circl.lu", "config": {"nameserver": "8.8.8.8"}} + response = self.misp_modules_post(query) + self.assertEqual(self.get_values(response), ['149.13.33.14']) + + def test_macvendors(self): + query = {"module": "macvendors", "mac-address": "FC-A1-3E-2A-1C-33"} + response = self.misp_modules_post(query) + self.assertEqual(self.get_values(response), 'Samsung Electronics Co.,Ltd') + + def test_haveibeenpwned(self): + query = {"module": "hibp", "email-src": "info@circl.lu"} + response = self.misp_modules_post(query) + self.assertEqual(self.get_values(response), 'OK (Not Found)') + + def test_greynoise(self): + query = {"module": "greynoise", "ip-dst": "1.1.1.1"} + response = self.misp_modules_post(query) + self.assertEqual(self.get_values(response)['status'], 'ok') + + def test_ipasn(self): + query = {"module": "ipasn", "ip-dst": "1.1.1.1"} + response = self.misp_modules_post(query) + key = list(self.get_values(response)['response'].keys())[0] + entry = self.get_values(response)['response'][key]['asn'] + self.assertEqual(entry, '13335') + + def test_bgpranking(self): + query = {"module": "bgpranking", "AS": "13335"} + response = self.misp_modules_post(query) + self.assertEqual(self.get_values(response)['response']['asn_description'], 'CLOUDFLARENET - Cloudflare, Inc., US') From 9ea9816ad3911f0833d337f7b44cd580e940b35b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Tue, 2 Apr 2019 15:31:50 +0200 Subject: [PATCH 058/207] chg: Bump dependencies --- Pipfile.lock | 180 +++++++++++++++++++++++++-------------------------- 1 file changed, 88 insertions(+), 92 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 60f1d03..b5faca1 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -253,39 +253,35 @@ }, "pillow": { "hashes": [ - "sha256:051de330a06c99d6f84bcf582960487835bcae3fc99365185dc2d4f65a390c0e", - "sha256:0ae5289948c5e0a16574750021bd8be921c27d4e3527800dc9c2c1d2abc81bf7", - "sha256:0b1efce03619cdbf8bcc61cfae81fcda59249a469f31c6735ea59badd4a6f58a", - "sha256:163136e09bd1d6c6c6026b0a662976e86c58b932b964f255ff384ecc8c3cefa3", - "sha256:18e912a6ccddf28defa196bd2021fe33600cbe5da1aa2f2e2c6df15f720b73d1", - "sha256:24ec3dea52339a610d34401d2d53d0fb3c7fd08e34b20c95d2ad3973193591f1", - "sha256:267f8e4c0a1d7e36e97c6a604f5b03ef58e2b81c1becb4fccecddcb37e063cc7", - "sha256:3273a28734175feebbe4d0a4cde04d4ed20f620b9b506d26f44379d3c72304e1", - "sha256:4c678e23006798fc8b6f4cef2eaad267d53ff4c1779bd1af8725cc11b72a63f3", - "sha256:4d4bc2e6bb6861103ea4655d6b6f67af8e5336e7216e20fff3e18ffa95d7a055", - "sha256:505738076350a337c1740a31646e1de09a164c62c07db3b996abdc0f9d2e50cf", - "sha256:5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f", - "sha256:5d95cb9f6cced2628f3e4de7e795e98b2659dfcc7176ab4a01a8b48c2c2f488f", - "sha256:7eda4c737637af74bac4b23aa82ea6fbb19002552be85f0b89bc27e3a762d239", - "sha256:801ddaa69659b36abf4694fed5aa9f61d1ecf2daaa6c92541bbbbb775d97b9fe", - "sha256:825aa6d222ce2c2b90d34a0ea31914e141a85edefc07e17342f1d2fdf121c07c", - "sha256:9c215442ff8249d41ff58700e91ef61d74f47dfd431a50253e1a1ca9436b0697", - "sha256:a3d90022f2202bbb14da991f26ca7a30b7e4c62bf0f8bf9825603b22d7e87494", - "sha256:a631fd36a9823638fe700d9225f9698fb59d049c942d322d4c09544dc2115356", - "sha256:a6523a23a205be0fe664b6b8747a5c86d55da960d9586db039eec9f5c269c0e6", - "sha256:a756ecf9f4b9b3ed49a680a649af45a8767ad038de39e6c030919c2f443eb000", - "sha256:b117287a5bdc81f1bac891187275ec7e829e961b8032c9e5ff38b70fd036c78f", - "sha256:ba04f57d1715ca5ff74bb7f8a818bf929a204b3b3c2c2826d1e1cc3b1c13398c", - "sha256:cd878195166723f30865e05d87cbaf9421614501a4bd48792c5ed28f90fd36ca", - "sha256:cee815cc62d136e96cf76771b9d3eb58e0777ec18ea50de5cfcede8a7c429aa8", - "sha256:d1722b7aa4b40cf93ac3c80d3edd48bf93b9208241d166a14ad8e7a20ee1d4f3", - "sha256:d7c1c06246b05529f9984435fc4fa5a545ea26606e7f450bdbe00c153f5aeaad", - "sha256:e9c8066249c040efdda84793a2a669076f92a301ceabe69202446abb4c5c5ef9", - "sha256:f227d7e574d050ff3996049e086e1f18c7bd2d067ef24131e50a1d3fe5831fbc", - "sha256:fc9a12aad714af36cf3ad0275a96a733526571e52710319855628f476dcb144e" + "sha256:15c056bfa284c30a7f265a41ac4cbbc93bdbfc0dfe0613b9cb8a8581b51a9e55", + "sha256:1a4e06ba4f74494ea0c58c24de2bb752818e9d504474ec95b0aa94f6b0a7e479", + "sha256:1c3c707c76be43c9e99cb7e3d5f1bee1c8e5be8b8a2a5eeee665efbf8ddde91a", + "sha256:1fd0b290203e3b0882d9605d807b03c0f47e3440f97824586c173eca0aadd99d", + "sha256:24114e4a6e1870c5a24b1da8f60d0ba77a0b4027907860188ea82bd3508c80eb", + "sha256:258d886a49b6b058cd7abb0ab4b2b85ce78669a857398e83e8b8e28b317b5abb", + "sha256:33c79b6dd6bc7f65079ab9ca5bebffb5f5d1141c689c9c6a7855776d1b09b7e8", + "sha256:367385fc797b2c31564c427430c7a8630db1a00bd040555dfc1d5c52e39fcd72", + "sha256:3c1884ff078fb8bf5f63d7d86921838b82ed4a7d0c027add773c2f38b3168754", + "sha256:44e5240e8f4f8861d748f2a58b3f04daadab5e22bfec896bf5434745f788f33f", + "sha256:46aa988e15f3ea72dddd81afe3839437b755fffddb5e173886f11460be909dce", + "sha256:74d90d499c9c736d52dd6d9b7221af5665b9c04f1767e35f5dd8694324bd4601", + "sha256:809c0a2ce9032cbcd7b5313f71af4bdc5c8c771cb86eb7559afd954cab82ebb5", + "sha256:85d1ef2cdafd5507c4221d201aaf62fc9276f8b0f71bd3933363e62a33abc734", + "sha256:8c3889c7681af77ecfa4431cd42a2885d093ecb811e81fbe5e203abc07e0995b", + "sha256:9218d81b9fca98d2c47d35d688a0cea0c42fd473159dfd5612dcb0483c63e40b", + "sha256:9aa4f3827992288edd37c9df345783a69ef58bd20cc02e64b36e44bcd157bbf1", + "sha256:9d80f44137a70b6f84c750d11019a3419f409c944526a95219bea0ac31f4dd91", + "sha256:b7ebd36128a2fe93991293f997e44be9286503c7530ace6a55b938b20be288d8", + "sha256:c4c78e2c71c257c136cdd43869fd3d5e34fc2162dc22e4a5406b0ebe86958239", + "sha256:c6a842537f887be1fe115d8abb5daa9bc8cc124e455ff995830cc785624a97af", + "sha256:cf0a2e040fdf5a6d95f4c286c6ef1df6b36c218b528c8a9158ec2452a804b9b8", + "sha256:cfd28aad6fc61f7a5d4ee556a997dc6e5555d9381d1390c00ecaf984d57e4232", + "sha256:dca5660e25932771460d4688ccbb515677caaf8595f3f3240ec16c117deff89a", + "sha256:de7aedc85918c2f887886442e50f52c1b93545606317956d65f342bd81cb4fc3", + "sha256:e6c0bbf8e277b74196e3140c35f9a1ae3eafd818f7f2d3a15819c49135d6c062" ], "index": "pypi", - "version": "==5.4.1" + "version": "==6.0.0" }, "psutil": { "hashes": [ @@ -304,7 +300,7 @@ "pybgpranking": { "editable": true, "git": "https://github.com/D4-project/BGP-Ranking.git/", - "ref": "4f2898af7c4e237b6497831d5acf3f4531ac14d8", + "ref": "019ef1c40aad1e5bb5c5072c9a998c6a8f0271f3", "subdirectory": "client" }, "pydnstrails": { @@ -335,13 +331,13 @@ "pyipasnhistory": { "editable": true, "git": "https://github.com/D4-project/IPASN-History.git/", - "ref": "7ef09cf761fc58aa774ea305a33ba75959e39887", + "ref": "0c4f11792061417b77ca6e22d2ece18109d74c75", "subdirectory": "client" }, "pymisp": { "editable": true, "git": "https://github.com/MISP/PyMISP.git", - "ref": "1dddfd72e08886673e57e23627064f9ea8303d4c" + "ref": "64bcaad0e578129543cdffad532a232722615f6c" }, "pyonyphe": { "editable": true, @@ -414,44 +410,44 @@ }, "redis": { "hashes": [ - "sha256:724932360d48e5407e8f82e405ab3650a36ed02c7e460d1e6fddf0f038422b54", - "sha256:9b19425a38fd074eb5795ff2b0d9a55b46a44f91f5347995f27e3ad257a7d775" + "sha256:6946b5dca72e86103edc8033019cc3814c031232d339d5f4533b02ea85685175", + "sha256:8ca418d2ddca1b1a850afa1680a7d2fd1f3322739271de4b704e0d4668449273" ], - "version": "==3.2.0" + "version": "==3.2.1" }, "reportlab": { "hashes": [ - "sha256:069f684cd0aaa518a27dc9124aed29cee8998e21ddf19604e53214ec8462bdd7", - "sha256:09b68ec01d86b4b120456b3f3202570ec96f57624e3a4fc36f3829323391daa4", - "sha256:0c32be9a406172c29ea20ff55a709ccac1e7fb09f15aba67cb7b455fd1d3dbe0", - "sha256:233196cf25e97cfe7c452524ea29d9a4909f1cb66599299233be1efaaaa7a7a3", - "sha256:2b5e4533f3e5b962835a5ce44467e66d1ecc822761d1b508077b5087a06be338", - "sha256:2e860bcdace5a558356802a92ae8658d7e5fdaa00ded82e83a3f2987c562cb66", - "sha256:3546029e63a9a9dc24ee38959eb417678c2425b96cd27b31e09e216dafc94666", - "sha256:4452b93f9c73b6b70311e7d69082d64da81b38e91bfb4766397630092e6da6fd", - "sha256:528c74a1c6527d1859c2c7a64a94a1cba485b00175162ea23699ae58a1e94939", - "sha256:6116e750f98018febc08dfee6df20446cf954adbcfa378d2c703d56c8864aff3", - "sha256:6b2b3580c647d75ef129172cb3da648cdb24566987b0b59c5ebb80ab770748d6", - "sha256:727b5f2bed08552d143fc99649b1863c773729f580a416844f9d9967bb0a1ae8", - "sha256:74c24a3ec0a3d4f8acb13a07192f45bdb54a1cc3c2286241677e7e8bcd5011fa", - "sha256:98ccd2f8b4f8636db05f3f14db0b471ad6bb4b66ae0dc9052c4822b3bd5d6a7d", - "sha256:a5905aa567946bc938b489a7249c7890c3fd3c9b7b5680dece5bc551c2ddbe0d", - "sha256:acbb7f676b8586b770719e9683eda951fdb38eb7970d46fcbf3cdda88d912a64", - "sha256:b5e30f865add48cf880f1c363eb505b97f2f7baaa88c155f87a335a76515a3e5", - "sha256:be2a7c33a2c28bbd3f453ffe4f0e5200b88c803a097f4cf52d69c6b53fad7a8f", - "sha256:c356bb600f59ac64955813d6497a08bfd5d0c451cb5829b61e3913d0ac084e26", - "sha256:c7ec4ae2393beab584921b1287a04e94fd98c28315e348362d89b85f4b464546", - "sha256:d476edc831bb3e9ebd04d1403abaf3ea57b3e4c2276c91a54fdfb6efbd3f9d97", - "sha256:db059e1a0691c872784062421ec51848539eb4f5210142682e61059a5ca7cc55", - "sha256:dd423a6753509ab14a0ac1b5be39d219c8f8d3781cce3deb4f45eda31969b5e8", - "sha256:ed9b7c0d71ce6fe2b31c6cde530ad8238632b876a5d599218739bda142a77f7c", - "sha256:f0a2465af4006f97b05e1f1546d67d3a3213d414894bf28be7f87f550a7f4a55", - "sha256:f20bfe26e57e8e1f575a9e0325be04dd3562db9f247ffdd73b5d4df6dec53bc2", - "sha256:f3463f2cb40a1b515ac0133ba859eca58f53b56760da9abb27ed684c565f853c", - "sha256:facc3c9748ab1525fb8401a1223bce4f24f0d6aa1a9db86c55db75777ccf40f9" + "sha256:0135bc54a463db5315c93bba4182fb83dc088fefaa7da18784ecd2a0c4a9c068", + "sha256:09e167e01458ea1e0cf3acff634ae9ecc1f1757e7585060d039c90b762859cfd", + "sha256:0dfcea18ba3ca1fac55cb273d056a8a43a48bd04d419299b3267e1994c72455a", + "sha256:1a61e56593ea1a8a38135eedfb40f79dcad13164fff034313ebf2a30e200ca79", + "sha256:1bdd871c2087d3853a0e9a3a573b1a7535500f3341944b1e34e68f3213cd28b8", + "sha256:26878a4b9c45f046c635b5695681188c19806f08b04129ea01c9ed51c7754039", + "sha256:27c62264c758aa30113df105da816223d149e4e87ee778ad49469725b79be2eb", + "sha256:29a9dd3954465b9e4efb129ffda9ab3e6a4f06488e8aa2efd5aff8ad332f13c2", + "sha256:5740e3218ca98c1bc86bd2d2e2a8c1d23e7c97d949d6377ac30aaf449f01c363", + "sha256:605892bb3f822a1e7342ce2b461d645ab8e4d13875127c0ae5377f76853db422", + "sha256:6dacc72552bc0dd50286e856f09a5e646a007d9345598bf6f75b117a200bfd9d", + "sha256:7021b7c8ba6d8e69e4c68c9473067482aaa40b9094270b45dbf798fcb0e09bd4", + "sha256:8acd950dad5b20a417579d1253c1065222dde48f9412e71533b052ab3dd98632", + "sha256:8b8fb3b0dd1e2124aba24544a02c95bff1fffa966b0581f30abf4fb28e414005", + "sha256:920c61c942eb1cc446e1647a04978f4afe31993ed403b74576a018c3ca526394", + "sha256:928e8d99befe064e28e9a29a4fd9afcf2066dcd758b0903280e67e221527422a", + "sha256:a04787eee401a74c80b65e539b5fe9226fdeabe25caa3d216c21dc990b2f8a01", + "sha256:a5bb6bd7753cba854425fcf7ecf04627a17de78d47ef9e8fac615887c5658da3", + "sha256:a70d970619014dc83b4406bcfed7e2f9d5aaf5f521aad808f5560d90ea896fb4", + "sha256:ae468fe82c8af3d1987113f03c1f87d01daa5b4c85c1f10da126be84423a744d", + "sha256:b278d83a7f76410bd310b368309e6e4b19664ffa686abfa9f0696130b09c17d3", + "sha256:b6623e9a96db3edc4b384e036e67c7bc87bbd7e5dc2d72ce66efa0043f9383b0", + "sha256:dc15cfa577bb25f0a598d483cf6dcc5ecad576ba723fe9bec63b6ec720dab2a3", + "sha256:dffdb4f6b34ce791e67365f3f96ab3c45b4cdd2c70d212fac98fb146dc75ac80", + "sha256:e84020e3482856da733e1359cb7b84e6bac09179bd3af860e70468a9c3cb43e3", + "sha256:edda09668e8474d5acb1a37fb64599557b43a714f1469bd49a058e95b5b410ff", + "sha256:f77e9835873931d25f836a3c107e53e0f7d3c0b4906b13063815308cf5ca1fac", + "sha256:f91d16ff07d5d3c92303f64c6864d74d3b6a491dde186bfef90c58088f932998" ], "index": "pypi", - "version": "==3.5.13" + "version": "==3.5.17" }, "requests": { "hashes": [ @@ -477,10 +473,10 @@ }, "sigmatools": { "hashes": [ - "sha256:3bdbd2ee99c32f245e948d6b882219729ab379685dd7366e4d6149c390e08170" + "sha256:ae980b6d6fd466294911efa493934d24e3c5df406da4a190b9fff0943a81cc5f" ], "index": "pypi", - "version": "==0.9" + "version": "==0.10" }, "six": { "hashes": [ @@ -491,10 +487,10 @@ }, "soupsieve": { "hashes": [ - "sha256:afa56bf14907bb09403e5d15fbed6275caa4174d36b975226e3b67a3bb6e2c4b", - "sha256:eaed742b48b1f3e2d45ba6f79401b2ed5dc33b2123dfe216adb90d4bfa0ade26" + "sha256:3aef141566afd07201b525c17bfaadd07580a8066f82b57f7c9417f26adbd0a3", + "sha256:e41a65e99bd125972d84221022beb1e4b5cfc68fa12c170c39834ce32d1b294c" ], - "version": "==1.8" + "version": "==1.9" }, "sparqlwrapper": { "hashes": [ @@ -520,15 +516,15 @@ }, "tornado": { "hashes": [ - "sha256:1a58f2d603476d5e462f7c28ca1dbb5ac7e51348b27a9cac849cdec3471101f8", - "sha256:33f93243cd46dd398e5d2bbdd75539564d1f13f25d704cfc7541db74066d6695", - "sha256:34e59401afcecf0381a28228daad8ed3275bcb726810654612d5e9c001f421b7", - "sha256:35817031611d2c296c69e5023ea1f9b5720be803e3bb119464bb2a0405d5cd70", - "sha256:666b335cef5cc2759c21b7394cff881f71559aaf7cb8c4458af5bb6cb7275b47", - "sha256:81203efb26debaaef7158187af45bc440796de9fb1df12a75b65fae11600a255", - "sha256:de274c65f45f6656c375cdf1759dbf0bc52902a1e999d12a35eb13020a641a53" + "sha256:1174dcb84d08887b55defb2cda1986faeeea715fff189ef3dc44cce99f5fca6b", + "sha256:2613fab506bd2aedb3722c8c64c17f8f74f4070afed6eea17f20b2115e445aec", + "sha256:44b82bc1146a24e5b9853d04c142576b4e8fa7a92f2e30bc364a85d1f75c4de2", + "sha256:457fcbee4df737d2defc181b9073758d73f54a6cfc1f280533ff48831b39f4a8", + "sha256:49603e1a6e24104961497ad0c07c799aec1caac7400a6762b687e74c8206677d", + "sha256:8c2f40b99a8153893793559919a355d7b74649a11e59f411b0b0a1793e160bc0", + "sha256:e1d897889c3b5a829426b7d52828fb37b28bc181cd598624e65c8be40ee3f7fa" ], - "version": "==6.0.1" + "version": "==6.0.2" }, "url-normalize": { "hashes": [ @@ -559,20 +555,20 @@ }, "vulners": { "hashes": [ - "sha256:08a7ccb2b210d45143354c6161c73fe209dc14fae8692e8b793b36b79330ad11", - "sha256:bfe2478cc11c69ba7e436d7a5df925e227565782c0bd603929fb3d612c73d78d", - "sha256:d035f6a883625878a1dc377830d17d9702ef138ca31569ac01cb8686874f89cd" + "sha256:6617d5904b5369507bc34105071d312e9e1c38d73654505e7b15b9a3f1325915", + "sha256:8b05d12a9dd7cbc07198a13281299a6e014ec348522e214b1efd097e194b7568", + "sha256:a19b02e0a112d70951e10c5abc1993f7f029234212828e1b617ab35f4e460a24" ], "index": "pypi", - "version": "==1.4.5" + "version": "==1.4.7" }, "wand": { "hashes": [ - "sha256:7d6b8dc9d4eaccc430b9c86e6b749013220c994970a3f39e902b397e2fa732c3", - "sha256:cc0b5c9cd50fecd10dc8888b739dd5984c6f8085d2954f34903b83ca39a91236" + "sha256:91810d241ab0851d40e67c946beb960b869c4f4160c397eac291ec6283ee3e3f", + "sha256:ae7c0958509a22f531b7b97e93adfd3f1208f0ac1c593af9e5f0cffa4ac06d5b" ], "index": "pypi", - "version": "==0.5.1" + "version": "==0.5.2" }, "xlsxwriter": { "hashes": [ @@ -719,11 +715,11 @@ }, "more-itertools": { "hashes": [ - "sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40", - "sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1" + "sha256:2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7", + "sha256:c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a" ], "markers": "python_version > '2.7'", - "version": "==6.0.0" + "version": "==7.0.0" }, "nose": { "hashes": [ @@ -764,11 +760,11 @@ }, "pytest": { "hashes": [ - "sha256:592eaa2c33fae68c7d75aacf042efc9f77b27c08a6224a4f59beab8d9a420523", - "sha256:ad3ad5c450284819ecde191a654c09b0ec72257a2c711b9633d677c71c9850c4" + "sha256:13c5e9fb5ec5179995e9357111ab089af350d788cbc944c628f3cde72285809b", + "sha256:f21d2f1fb8200830dcbb5d8ec466a9c9120e20d8b53c7585d180125cce1d297a" ], "index": "pypi", - "version": "==4.3.1" + "version": "==4.4.0" }, "requests": { "hashes": [ From c64f514a6fdfb21397dcddd8feddf7ee90bf5767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Tue, 2 Apr 2019 15:39:27 +0200 Subject: [PATCH 059/207] fix: Typos in variable names --- misp_modules/modules/expansion/greynoise.py | 2 +- misp_modules/modules/expansion/hibp.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/misp_modules/modules/expansion/greynoise.py b/misp_modules/modules/expansion/greynoise.py index 324423d..66f6985 100644 --- a/misp_modules/modules/expansion/greynoise.py +++ b/misp_modules/modules/expansion/greynoise.py @@ -37,4 +37,4 @@ def introspection(): def version(): moduleinfo['config'] = moduleconfig - return moduleinf + return moduleinfo diff --git a/misp_modules/modules/expansion/hibp.py b/misp_modules/modules/expansion/hibp.py index 7010aee..67af644 100644 --- a/misp_modules/modules/expansion/hibp.py +++ b/misp_modules/modules/expansion/hibp.py @@ -37,4 +37,4 @@ def introspection(): def version(): moduleinfo['config'] = moduleconfig - return moduleinf + return moduleinfo From 9cb21f98e1b6670d733940ea74d75a7a01a1b38e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Tue, 2 Apr 2019 15:46:17 +0200 Subject: [PATCH 060/207] fix: Add the new module sin the list of modules availables. --- misp_modules/modules/expansion/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/__init__.py b/misp_modules/modules/expansion/__init__.py index d8fb153..42f74a3 100644 --- a/misp_modules/modules/expansion/__init__.py +++ b/misp_modules/modules/expansion/__init__.py @@ -8,4 +8,4 @@ __all__ = ['vmray_submit', 'bgpranking', 'circl_passivedns', 'circl_passivessl', 'yara_syntax_validator', 'hashdd', 'onyphe', 'onyphe_full', 'rbl', 'xforceexchange', 'sigma_syntax_validator', 'stix2_pattern_syntax_validator', 'sigma_queries', 'dbl_spamhaus', 'vulners', 'yara_query', 'macaddress_io', - 'intel471', 'backscatter_io', 'btc_scam_check'] + 'intel471', 'backscatter_io', 'btc_scam_check', 'hibp', 'greynoise', 'macvendors'] From f82933779f1567e1d4325f86a29b4459c9d624cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Tue, 2 Apr 2019 16:01:33 +0200 Subject: [PATCH 061/207] fix: pep8 foobar. --- misp_modules/modules/expansion/greynoise.py | 15 +++++++++------ misp_modules/modules/expansion/hibp.py | 17 ++++++++++------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/misp_modules/modules/expansion/greynoise.py b/misp_modules/modules/expansion/greynoise.py index 66f6985..d26736d 100644 --- a/misp_modules/modules/expansion/greynoise.py +++ b/misp_modules/modules/expansion/greynoise.py @@ -4,11 +4,12 @@ import json misperrors = {'error': 'Error'} mispattributes = {'input': ['ip-dst', 'ip-src'], 'output': ['text']} moduleinfo = {'version': '0.1', 'author': 'Aurélien Schwab ', 'description': 'Module to access GreyNoise.io API.', 'module-type': ['hover']} -moduleconfig = ['user-agent']#TODO take this into account in the code +moduleconfig = ['user-agent'] # TODO take this into account in the code greynoise_api_url = 'http://api.greynoise.io:8888/v1/query/ip' default_user_agent = 'MISP-Module' + def handler(q=False): if q is False: return False @@ -20,21 +21,23 @@ def handler(q=False): else: misperrors['error'] = "Unsupported attributes type" return misperrors - data = {'ip':ip} - r = requests.post(greynoise_api_url, data=data, headers={'user-agent': default_user_agent})#Real request - if r.status_code == 200:#OK (record found) + data = {'ip': ip} + r = requests.post(greynoise_api_url, data=data, headers={'user-agent': default_user_agent}) # Real request + if r.status_code == 200: # OK (record found) response = json.loads(r.text) if response: return {'results': [{'types': mispattributes['output'], 'values': response}]} - elif r.status_code == 404:#Not found (not an error) + elif r.status_code == 404: # Not found (not an error) return {'results': [{'types': mispattributes['output'], 'values': 'No data'}]} - else:#Real error + else: # Real error misperrors['error'] = 'GreyNoise API not accessible (HTTP ' + str(r.status_code) + ')' return misperrors['error'] + def introspection(): return mispattributes + def version(): moduleinfo['config'] = moduleconfig return moduleinfo diff --git a/misp_modules/modules/expansion/hibp.py b/misp_modules/modules/expansion/hibp.py index 67af644..8db3fa7 100644 --- a/misp_modules/modules/expansion/hibp.py +++ b/misp_modules/modules/expansion/hibp.py @@ -2,12 +2,13 @@ import requests import json misperrors = {'error': 'Error'} -mispattributes = {'input': ['email-dst', 'email-src'], 'output': ['text']}#All mails as input +mispattributes = {'input': ['email-dst', 'email-src'], 'output': ['text']} # All mails as input moduleinfo = {'version': '0.1', 'author': 'Aurélien Schwab', 'description': 'Module to access haveibeenpwned.com API.', 'module-type': ['hover']} -moduleconfig = ['user-agent']#TODO take this into account in the code +moduleconfig = ['user-agent'] # TODO take this into account in the code haveibeenpwned_api_url = 'https://api.haveibeenpwned.com/api/v2/breachedaccount/' -default_user_agent = 'MISP-Module'#User agent (must be set, requiered by API)) +default_user_agent = 'MISP-Module' # User agent (must be set, requiered by API)) + def handler(q=False): if q is False: @@ -21,20 +22,22 @@ def handler(q=False): misperrors['error'] = "Unsupported attributes type" return misperrors - r = requests.get(haveibeenpwned_api_url + email, headers={'user-agent': default_user_agent})#Real request - if r.status_code == 200:##OK (record found) + r = requests.get(haveibeenpwned_api_url + email, headers={'user-agent': default_user_agent}) # Real request + if r.status_code == 200: # OK (record found) breaches = json.loads(r.text) if breaches: return {'results': [{'types': mispattributes['output'], 'values': breaches}]} - elif r.status_code == 404:#Not found (not an error) + elif r.status_code == 404: # Not found (not an error) return {'results': [{'types': mispattributes['output'], 'values': 'OK (Not Found)'}]} - else:#Real error + else: # Real error misperrors['error'] = 'haveibeenpwned.com API not accessible (HTTP ' + str(r.status_code) + ')' return misperrors['error'] + def introspection(): return mispattributes + def version(): moduleinfo['config'] = moduleconfig return moduleinfo From 07a66d62b065bfe06225f5a0d1f6d95247e6d9b2 Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Tue, 2 Apr 2019 20:03:11 +0200 Subject: [PATCH 062/207] chg: [doc] new modules added --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index e890433..5669b19 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,14 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ * [EUPI](misp_modules/modules/expansion/eupi.py) - a hover and expansion module to get information about an URL from the [Phishing Initiative project](https://phishing-initiative.eu/?lang=en). * [Farsight DNSDB Passive DNS](misp_modules/modules/expansion/farsight_passivedns.py) - a hover and expansion module to expand hostname and IP addresses with passive DNS information. * [GeoIP](misp_modules/modules/expansion/geoip_country.py) - a hover and expansion module to get GeoIP information from geolite/maxmind. +* [Greynoise](misp_modules/modules/expansion/greynoise.py) - a hover to get information from greynoise. * [hashdd](misp_modules/modules/expansion/hashdd.py) - a hover module to check file hashes against [hashdd.com](http://www.hashdd.com) including NSLR dataset. +* [hibp](misp_modules/modules/expansion/hibp.py) - a hover module to lookup against Have I Been Pwned? * [intel471](misp_modules/modules/expansion/intel471.py) - an expansion module to get info from [Intel471](https://intel471.com). * [IPASN](misp_modules/modules/expansion/ipasn.py) - a hover and expansion to get the BGP ASN of an IP address. * [iprep](misp_modules/modules/expansion/iprep.py) - an expansion module to get IP reputation from packetmail.net. * [macaddress.io](misp_modules/modules/expansion/macaddress_io.py) - a hover module to retrieve vendor details and other information regarding a given MAC address or an OUI from [MAC address Vendor Lookup](https://macaddress.io). See [integration tutorial here](https://macaddress.io/integrations/MISP-module). +* [macvendors](misp_modules/modules/expansion/macvendors.py) - a hover module to retrieve mac vendor information. * [onyphe](misp_modules/modules/expansion/onyphe.py) - a modules to process queries on Onyphe. * [onyphe_full](misp_modules/modules/expansion/onyphe_full.py) - a modules to process full queries on Onyphe. * [OTX](misp_modules/modules/expansion/otx.py) - an expansion module for [OTX](https://otx.alienvault.com/). From 5ed91dcec2bf4509a5b3142f29c9ae417e6e1c02 Mon Sep 17 00:00:00 2001 From: Steve Clement Date: Mon, 8 Apr 2019 16:03:41 +0900 Subject: [PATCH 063/207] fix: [doc] Small typo fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5669b19..18b5548 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ scl enable rh-python36 ‘python3 –m pip install cryptography’ scl enable rh-python36 ‘python3 –m pip install -I -r REQUIREMENTS’ scl enable rh-python36 ‘python3 –m pip install –I .’ ~~~~ -Create the service file /etc/systemd/system/misp-workers.service : +Create the service file /etc/systemd/system/misp-modules.service : ~~~~ [Unit] Description=MISP's modules From b5f2424f274813e044ba88210628575855263b5e Mon Sep 17 00:00:00 2001 From: Steve Clement Date: Mon, 8 Apr 2019 16:17:22 +0900 Subject: [PATCH 064/207] chg: [doc] Updated README to reflect current virtualenv efforts. TODO: pipenv --- README.md | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 18b5548..400356e 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ * [ThreatAnalyzer](misp_modules/modules/import_mod/threatanalyzer_import.py) - An import module to process ThreatAnalyzer archive.zip/analysis.json sandbox exports. * [VMRay](misp_modules/modules/import_mod/vmray_import.py) - An import module to process VMRay export. -## How to install and start MISP modules in a Python virtualenv? +## How to install and start MISP modules in a Python virtualenv? (recommended) ~~~~bash sudo apt-get install python3-dev python3-pip libpq5 libjpeg-dev tesseract-ocr imagemagick virtualenv @@ -97,7 +97,10 @@ sudo git clone https://github.com/MISP/misp-modules.git cd misp-modules sudo -u www-data /var/www/MISP/venv/bin/pip install -I -r REQUIREMENTS sudo -u www-data /var/www/MISP/venv/bin/pip install . -sudo sed -i -e '$i \sudo -u www-data /var/www/MISP/venv/bin/misp-modules -l 127.0.0.1 -s > /tmp/misp-modules_rc.local.log &\n' /etc/rc.local +# Start misp-modules as a service +sudo cp etc/systemd/system/misp-modules.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now misp-modules /var/www/MISP/venv/bin/misp-modules -l 127.0.0.1 -s & #to start the modules ~~~~ @@ -110,7 +113,10 @@ sudo git clone https://github.com/MISP/misp-modules.git cd misp-modules sudo pip3 install -I -r REQUIREMENTS sudo pip3 install -I . -sudo sed -i -e '$i \sudo -u www-data /var/www/MISP/venv/bin/misp-modules -l 127.0.0.1 -s > /tmp/misp-modules_rc.local.log &\n' /etc/rc.local +# Start misp-modules as a service +sudo cp etc/systemd/system/misp-modules.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now misp-modules /var/www/MISP/venv/bin/misp-modules -l 127.0.0.1 -s & #to start the modules ~~~~ @@ -490,7 +496,7 @@ Download a pre-built virtual image from the [MISP training materials](https://ww - Create a Host-Only adapter in VirtualBox - Set your Misp OVA to that Host-Only adapter - Start the virtual machine -- Get the IP address of the virutal machine +- Get the IP address of the virtual machine - SSH into the machine (Login info on training page) - Go into the misp-modules directory @@ -510,14 +516,16 @@ Remove the contents of the build directory and re-install misp-modules. ~~~bash sudo rm -fr build/* -sudo pip3 install --upgrade . +sudo -u www-data /var/www/MISP/venv/bin/pip install --upgrade . ~~~ SSH in with a different terminal and run `misp-modules` with debugging enabled. ~~~bash -sudo killall misp-modules -misp-modules -d +# In case misp-modules is not a service do: +# sudo killall misp-modules +sudo systemctl disable --now misp-modules +sudo -u www-data /var/www/MISP/venv/bin/misp-modules -d ~~~ From d24a6e2e249f2d36a92634197c4e4c21a1b3b9b6 Mon Sep 17 00:00:00 2001 From: iceone23 Date: Mon, 15 Apr 2019 06:17:27 -0700 Subject: [PATCH 065/207] Create cisco_firesight_manager_ACL_rule_export.py Cisco Firesight Manager ACL Rule Export module --- ...cisco_firesight_manager_ACL_rule_export.py | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 misp_modules/modules/export_mod/cisco_firesight_manager_ACL_rule_export.py diff --git a/misp_modules/modules/export_mod/cisco_firesight_manager_ACL_rule_export.py b/misp_modules/modules/export_mod/cisco_firesight_manager_ACL_rule_export.py new file mode 100644 index 0000000..dcb6178 --- /dev/null +++ b/misp_modules/modules/export_mod/cisco_firesight_manager_ACL_rule_export.py @@ -0,0 +1,135 @@ +###################################################### +# # +# Author: Stanislav Klevtsov, Ukraine; Feb 2019. # +# # +# # +# Script was tested on the following configuration: # +# MISP v2.4.90 # +# Cisco Firesight Manager Console v6.2.3 (bld 84) # +# # +###################################################### + +import json +import base64 +import csv +from urllib.parse import quote + +misperrors = {'error': 'Error'} + +moduleinfo = {'version': '1', 'author': 'Stanislav Klevtsov', + 'description': 'Export malicious network activity attributes of the MISP event to Cisco firesight manager block rules', + 'module-type': ['export']} + + +moduleconfig = ['fmc_ip_addr', 'fmc_login', 'fmc_pass', 'domain_id', 'acpolicy_id'] + +fsmapping = {"ip-dst":"dst", "url":"request"} + +mispattributes = {'input':list(fsmapping.keys())} + +# options: event, attribute, event-collection, attribute-collection +inputSource = ['event'] + +outputFileExtension = 'sh' +responseType = 'application/txt' + +# .sh file templates +SH_FILE_HEADER = """#!/bin/sh\n\n""" + +BLOCK_JSON_TMPL = """ +BLOCK_RULE='{{ "action": "BLOCK", "enabled": true, "type": "AccessRule", "name": "{rule_name}", "destinationNetworks": {{ "literals": [ {dst_networks} ] }}, "urls": {{ "literals": [ {urls} ] }}, "newComments": [ "{event_info_comment}" ] }}'\n +""" + +BLOCK_DST_JSON_TMPL = """{{ "type": "Host", "value": "{ipdst}" }} """ +BLOCK_URL_JSON_TMPL = """{{ "type": "Url", "url": "{url}" }} """ + +CURL_ADD_RULE_TMPL = """ +curl -X POST -v -k -H 'Content-Type: application/json' -H \"Authorization: Basic $LOGINPASS_BASE64\" -H \"X-auth-access-token: $ACC_TOKEN\" -i \"https://$FIRESIGHT_IP_ADDR/api/fmc_config/v1/domain/$DOMAIN_ID/policy/accesspolicies/$ACPOLICY_ID/accessrules\" --data \"$BLOCK_RULE\" """ + + +def handler(q=False): + if q is False: + return False + + r = {'results': []} + request = json.loads(q) + + if "config" in request: + config = request["config"] + + #check if config is empty + if not config['fmc_ip_addr']: config['fmc_ip_addr'] = "0.0.0.0" + if not config['fmc_login']: config['fmc_login'] = "login" + if not config['fmc_pass']: config['fmc_pass'] = "password" + if not config['domain_id']: config['domain_id'] = "SET_FIRESIGHT_DOMAIN_ID" + if not config['acpolicy_id']: config['acpolicy_id'] = "SET_FIRESIGHT_ACPOLICY_ID" + + data = request["data"] + output = "" + ipdst = [] + urls = [] + + #populate the ACL rule with attributes + for ev in data: + + event = ev["Attribute"] + event_id = ev["Event"]["id"] + event_info = ev["Event"]["info"] + + for index, attr in enumerate(event): + if attr["to_ids"] is True: + + if attr["type"] in fsmapping: + if attr["type"] in "ip-dst": + ipdst.append(BLOCK_DST_JSON_TMPL.format(ipdst=attr["value"])) + else: + urls.append(BLOCK_URL_JSON_TMPL.format(url=quote(attr["value"], safe='@/:;?&=-_.,+!*'))) + + + #building the .sh file + output += SH_FILE_HEADER + output += "FIRESIGHT_IP_ADDR='" + config['fmc_ip_addr'] + "'\n" + + output += "LOGINPASS_BASE64=`echo -n '" + config['fmc_login'] + ":" + config['fmc_pass'] + "' | base64`\n" + output += "DOMAIN_ID='" + config['domain_id'] + "'\n" + output += "ACPOLICY_ID='" + config['acpolicy_id'] + "'\n\n" + + output += "ACC_TOKEN=`curl -X POST -v -k -sD - -o /dev/null -H \"Authorization: Basic $LOGINPASS_BASE64\" -i \"https://$FIRESIGHT_IP_ADDR/api/fmc_platform/v1/auth/generatetoken\" | grep -i x-auth-acc | sed 's/.*:\\ //g' | tr -d '[:space:]' | tr -d '\\n'`" + "\n" + + output += BLOCK_JSON_TMPL.format(rule_name="misp_event_"+event_id,dst_networks=', '.join(ipdst), urls=', '.join(urls), event_info_comment=event_info) + "\n" + + output += CURL_ADD_RULE_TMPL + # END building the .sh file + + r = {"data":base64.b64encode(output.encode('utf-8')).decode('utf-8')} + return r + + +def introspection(): + modulesetup = {} + try: + responseType + modulesetup['responseType'] = responseType + except NameError: + pass + try: + userConfig + modulesetup['userConfig'] = userConfig + except NameError: + pass + try: + outputFileExtension + modulesetup['outputFileExtension'] = outputFileExtension + except NameError: + pass + try: + inputSource + modulesetup['inputSource'] = inputSource + except NameError: + pass + return modulesetup + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo From f5167c2f230c7dea0725bdb585c6ce8d61496942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Tue, 16 Apr 2019 11:25:39 +0200 Subject: [PATCH 066/207] fix: Make flake8 happy. --- ...cisco_firesight_manager_ACL_rule_export.py | 147 +++++++++--------- 1 file changed, 76 insertions(+), 71 deletions(-) diff --git a/misp_modules/modules/export_mod/cisco_firesight_manager_ACL_rule_export.py b/misp_modules/modules/export_mod/cisco_firesight_manager_ACL_rule_export.py index dcb6178..ab79692 100644 --- a/misp_modules/modules/export_mod/cisco_firesight_manager_ACL_rule_export.py +++ b/misp_modules/modules/export_mod/cisco_firesight_manager_ACL_rule_export.py @@ -11,21 +11,20 @@ import json import base64 -import csv from urllib.parse import quote misperrors = {'error': 'Error'} moduleinfo = {'version': '1', 'author': 'Stanislav Klevtsov', - 'description': 'Export malicious network activity attributes of the MISP event to Cisco firesight manager block rules', - 'module-type': ['export']} + 'description': 'Export malicious network activity attributes of the MISP event to Cisco firesight manager block rules', + 'module-type': ['export']} moduleconfig = ['fmc_ip_addr', 'fmc_login', 'fmc_pass', 'domain_id', 'acpolicy_id'] -fsmapping = {"ip-dst":"dst", "url":"request"} +fsmapping = {"ip-dst": "dst", "url": "request"} -mispattributes = {'input':list(fsmapping.keys())} +mispattributes = {'input': list(fsmapping.keys())} # options: event, attribute, event-collection, attribute-collection inputSource = ['event'] @@ -48,88 +47,94 @@ curl -X POST -v -k -H 'Content-Type: application/json' -H \"Authorization: Basic def handler(q=False): - if q is False: - return False - - r = {'results': []} - request = json.loads(q) + if q is False: + return False - if "config" in request: - config = request["config"] + r = {'results': []} + request = json.loads(q) - #check if config is empty - if not config['fmc_ip_addr']: config['fmc_ip_addr'] = "0.0.0.0" - if not config['fmc_login']: config['fmc_login'] = "login" - if not config['fmc_pass']: config['fmc_pass'] = "password" - if not config['domain_id']: config['domain_id'] = "SET_FIRESIGHT_DOMAIN_ID" - if not config['acpolicy_id']: config['acpolicy_id'] = "SET_FIRESIGHT_ACPOLICY_ID" + if "config" in request: + config = request["config"] - data = request["data"] - output = "" - ipdst = [] - urls = [] + # check if config is empty + if not config['fmc_ip_addr']: + config['fmc_ip_addr'] = "0.0.0.0" + if not config['fmc_login']: + config['fmc_login'] = "login" + if not config['fmc_pass']: + config['fmc_pass'] = "password" + if not config['domain_id']: + config['domain_id'] = "SET_FIRESIGHT_DOMAIN_ID" + if not config['acpolicy_id']: + config['acpolicy_id'] = "SET_FIRESIGHT_ACPOLICY_ID" - #populate the ACL rule with attributes - for ev in data: + data = request["data"] + output = "" + ipdst = [] + urls = [] - event = ev["Attribute"] - event_id = ev["Event"]["id"] - event_info = ev["Event"]["info"] + # populate the ACL rule with attributes + for ev in data: - for index, attr in enumerate(event): - if attr["to_ids"] is True: + event = ev["Attribute"] + event_id = ev["Event"]["id"] + event_info = ev["Event"]["info"] - if attr["type"] in fsmapping: - if attr["type"] in "ip-dst": - ipdst.append(BLOCK_DST_JSON_TMPL.format(ipdst=attr["value"])) - else: - urls.append(BLOCK_URL_JSON_TMPL.format(url=quote(attr["value"], safe='@/:;?&=-_.,+!*'))) + for index, attr in enumerate(event): + if attr["to_ids"] is True: + if attr["type"] in fsmapping: + if attr["type"] == "ip-dst": + ipdst.append(BLOCK_DST_JSON_TMPL.format(ipdst=attr["value"])) + else: + urls.append(BLOCK_URL_JSON_TMPL.format(url=quote(attr["value"], safe='@/:;?&=-_.,+!*'))) + # building the .sh file + output += SH_FILE_HEADER + output += "FIRESIGHT_IP_ADDR='{}'\n".format(config['fmc_ip_addr']) - #building the .sh file - output += SH_FILE_HEADER - output += "FIRESIGHT_IP_ADDR='" + config['fmc_ip_addr'] + "'\n" + output += "LOGINPASS_BASE64=`echo -n '{}:{}' | base64`\n".format(config['fmc_login'], config['fmc_pass']) + output += "DOMAIN_ID='{}'\n".format(config['domain_id']) + output += "ACPOLICY_ID='{}'\n\n".format(config['acpolicy_id']) - output += "LOGINPASS_BASE64=`echo -n '" + config['fmc_login'] + ":" + config['fmc_pass'] + "' | base64`\n" - output += "DOMAIN_ID='" + config['domain_id'] + "'\n" - output += "ACPOLICY_ID='" + config['acpolicy_id'] + "'\n\n" - - output += "ACC_TOKEN=`curl -X POST -v -k -sD - -o /dev/null -H \"Authorization: Basic $LOGINPASS_BASE64\" -i \"https://$FIRESIGHT_IP_ADDR/api/fmc_platform/v1/auth/generatetoken\" | grep -i x-auth-acc | sed 's/.*:\\ //g' | tr -d '[:space:]' | tr -d '\\n'`" + "\n" + output += "ACC_TOKEN=`curl -X POST -v -k -sD - -o /dev/null -H \"Authorization: Basic $LOGINPASS_BASE64\" -i \"https://$FIRESIGHT_IP_ADDR/api/fmc_platform/v1/auth/generatetoken\" | grep -i x-auth-acc | sed 's/.*:\\ //g' | tr -d '[:space:]' | tr -d '\\n'`\n" - output += BLOCK_JSON_TMPL.format(rule_name="misp_event_"+event_id,dst_networks=', '.join(ipdst), urls=', '.join(urls), event_info_comment=event_info) + "\n" + output += BLOCK_JSON_TMPL.format(rule_name="misp_event_{}".format(event_id), + dst_networks=', '.join(ipdst), + urls=', '.join(urls), + event_info_comment=event_info) + "\n" - output += CURL_ADD_RULE_TMPL - # END building the .sh file + output += CURL_ADD_RULE_TMPL + # END building the .sh file - r = {"data":base64.b64encode(output.encode('utf-8')).decode('utf-8')} - return r + r = {"data": base64.b64encode(output.encode('utf-8')).decode('utf-8')} + return r def introspection(): - modulesetup = {} - try: - responseType - modulesetup['responseType'] = responseType - except NameError: - pass - try: - userConfig - modulesetup['userConfig'] = userConfig - except NameError: - pass - try: - outputFileExtension - modulesetup['outputFileExtension'] = outputFileExtension - except NameError: - pass - try: - inputSource - modulesetup['inputSource'] = inputSource - except NameError: - pass - return modulesetup + modulesetup = {} + try: + responseType + modulesetup['responseType'] = responseType + except NameError: + pass + try: + userConfig + modulesetup['userConfig'] = userConfig + except NameError: + pass + try: + outputFileExtension + modulesetup['outputFileExtension'] = outputFileExtension + except NameError: + pass + try: + inputSource + modulesetup['inputSource'] = inputSource + except NameError: + pass + return modulesetup def version(): - moduleinfo['config'] = moduleconfig - return moduleinfo + moduleinfo['config'] = moduleconfig + return moduleinfo From 639534f152f9e94f529bc0c8a55847ddcd1d00f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Tue, 16 Apr 2019 11:25:53 +0200 Subject: [PATCH 067/207] chg: Bump Dependencies. --- Pipfile.lock | 106 +++++++++++++++++++++++++-------------------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index b5faca1..428ae11 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -111,10 +111,10 @@ }, "click-plugins": { "hashes": [ - "sha256:b1ee1ccc9421c73007fe290680d97984eb6eaf5f4512b7620c6aa46031d6cb6b", - "sha256:dfed74b5063546a137de99baaaf742b4de4337ad2b3e1df5ec7c8a256adc0847" + "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b", + "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8" ], - "version": "==1.0.4" + "version": "==1.1.1" }, "colorama": { "hashes": [ @@ -300,7 +300,7 @@ "pybgpranking": { "editable": true, "git": "https://github.com/D4-project/BGP-Ranking.git/", - "ref": "019ef1c40aad1e5bb5c5072c9a998c6a8f0271f3", + "ref": "86180c080ba5f83a5160a8295ebc263709aa4eba", "subdirectory": "client" }, "pydnstrails": { @@ -331,13 +331,13 @@ "pyipasnhistory": { "editable": true, "git": "https://github.com/D4-project/IPASN-History.git/", - "ref": "0c4f11792061417b77ca6e22d2ece18109d74c75", + "ref": "1009124c39e9cc3b362f0f9ce4d272817b9a5186", "subdirectory": "client" }, "pymisp": { "editable": true, "git": "https://github.com/MISP/PyMISP.git", - "ref": "64bcaad0e578129543cdffad532a232722615f6c" + "ref": "13445b51748524195bb15aec2c1c6176bcb6bd95" }, "pyonyphe": { "editable": true, @@ -346,10 +346,10 @@ }, "pyparsing": { "hashes": [ - "sha256:66c9268862641abcac4a96ba74506e594c884e3f57690a696d21ad8210ed667a", - "sha256:f6c5ef0d7480ad048c054c37632c67fca55299990fff127850181659eea33fc3" + "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a", + "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03" ], - "version": "==2.3.1" + "version": "==2.4.0" }, "pypdns": { "hashes": [ @@ -417,37 +417,37 @@ }, "reportlab": { "hashes": [ - "sha256:0135bc54a463db5315c93bba4182fb83dc088fefaa7da18784ecd2a0c4a9c068", - "sha256:09e167e01458ea1e0cf3acff634ae9ecc1f1757e7585060d039c90b762859cfd", - "sha256:0dfcea18ba3ca1fac55cb273d056a8a43a48bd04d419299b3267e1994c72455a", - "sha256:1a61e56593ea1a8a38135eedfb40f79dcad13164fff034313ebf2a30e200ca79", - "sha256:1bdd871c2087d3853a0e9a3a573b1a7535500f3341944b1e34e68f3213cd28b8", - "sha256:26878a4b9c45f046c635b5695681188c19806f08b04129ea01c9ed51c7754039", - "sha256:27c62264c758aa30113df105da816223d149e4e87ee778ad49469725b79be2eb", - "sha256:29a9dd3954465b9e4efb129ffda9ab3e6a4f06488e8aa2efd5aff8ad332f13c2", - "sha256:5740e3218ca98c1bc86bd2d2e2a8c1d23e7c97d949d6377ac30aaf449f01c363", - "sha256:605892bb3f822a1e7342ce2b461d645ab8e4d13875127c0ae5377f76853db422", - "sha256:6dacc72552bc0dd50286e856f09a5e646a007d9345598bf6f75b117a200bfd9d", - "sha256:7021b7c8ba6d8e69e4c68c9473067482aaa40b9094270b45dbf798fcb0e09bd4", - "sha256:8acd950dad5b20a417579d1253c1065222dde48f9412e71533b052ab3dd98632", - "sha256:8b8fb3b0dd1e2124aba24544a02c95bff1fffa966b0581f30abf4fb28e414005", - "sha256:920c61c942eb1cc446e1647a04978f4afe31993ed403b74576a018c3ca526394", - "sha256:928e8d99befe064e28e9a29a4fd9afcf2066dcd758b0903280e67e221527422a", - "sha256:a04787eee401a74c80b65e539b5fe9226fdeabe25caa3d216c21dc990b2f8a01", - "sha256:a5bb6bd7753cba854425fcf7ecf04627a17de78d47ef9e8fac615887c5658da3", - "sha256:a70d970619014dc83b4406bcfed7e2f9d5aaf5f521aad808f5560d90ea896fb4", - "sha256:ae468fe82c8af3d1987113f03c1f87d01daa5b4c85c1f10da126be84423a744d", - "sha256:b278d83a7f76410bd310b368309e6e4b19664ffa686abfa9f0696130b09c17d3", - "sha256:b6623e9a96db3edc4b384e036e67c7bc87bbd7e5dc2d72ce66efa0043f9383b0", - "sha256:dc15cfa577bb25f0a598d483cf6dcc5ecad576ba723fe9bec63b6ec720dab2a3", - "sha256:dffdb4f6b34ce791e67365f3f96ab3c45b4cdd2c70d212fac98fb146dc75ac80", - "sha256:e84020e3482856da733e1359cb7b84e6bac09179bd3af860e70468a9c3cb43e3", - "sha256:edda09668e8474d5acb1a37fb64599557b43a714f1469bd49a058e95b5b410ff", - "sha256:f77e9835873931d25f836a3c107e53e0f7d3c0b4906b13063815308cf5ca1fac", - "sha256:f91d16ff07d5d3c92303f64c6864d74d3b6a491dde186bfef90c58088f932998" + "sha256:1c228a3ac2c405f7fc16eac43ba92aec448bc25438902f30590ad021e8828097", + "sha256:2210fafd3bb06308a84876fe6d19172b645373edce2b6d7501378cb9c768f825", + "sha256:232fb2037b7c3df259685f1c5ecb7826f55742dc81f0713837b84a152307483e", + "sha256:2c4f25e63fa75f3064871cf435696a4e19b7bd4901d922b766ae58a447b5b6da", + "sha256:47951166d897b60e9e7ca349db82a2b689e6478ac6078e2c7c88ca8becbb0c7d", + "sha256:526ab1193ea8e97c4838135917890e66de5f777d04283008007229b139f3c094", + "sha256:5a9cc8470623ec5b76c7e59f56b7d1fcf0254896cd61842dbdbd278934cc50f4", + "sha256:5ddc1a4a74f225e35a7f60e2eae10de6878dddc9960dad2d9cadc49092f8850d", + "sha256:6b594f6d7d71bc5778e19adb1c699a598c69b9a7bcf97fa638d8762279f9d80a", + "sha256:6e8c89b46cfaf9ae40b7db87e9f29c9e5d32d18d25f9cd10d423a5241e8ec453", + "sha256:71f4f3e3975b91ddbfc1b36a537b46d07533ca7f31945e990a75db5f9bd7a0ba", + "sha256:763654dc346eeb66fa726a88d27f911339950d20a25303dfc098f3b59ba26614", + "sha256:7bae4b33363f44343e0fac5004c8e44576c3ed00885be4eee1f2260802c116c3", + "sha256:8a4b8a0fd0547f3b436b548284aa604ba183bfac26f41a7ffb23d0ff5db8c658", + "sha256:8b08d68e4cb498eabf85411beda5c32e591ef8d0a6d18c948c3f80ed5d2c6e31", + "sha256:9840f27948b54aefa3c6386e5ed0f124d641eb54fa2f2bc9aebcb270598487fc", + "sha256:9ae8f822370e47486ba1880f7580669058a41e64bdaa41019f4617317489f884", + "sha256:9db49197080646a113059eba1c0758161164de1bc57315e7422bbf8c86e03dcf", + "sha256:a08d23fa3f23f13a1cc6dca3b3c431d08ae48e52384e6bf47bbefb22fde58e61", + "sha256:ac111bc47733dbfa3e34d61282c91b69b1f66800b0c72b7b86dc2534faa09bef", + "sha256:bc3c69707c0bf9308193612d34ca87249d6fc91a35ce0873102321395d39024a", + "sha256:c375759a763c1c93d5b4f36620390440d9fa6dec6fcf88bce8234701d88b339c", + "sha256:c8a5988d73ec93a54f22660b64c5f3d2018163dd9ca4a5cdde8022a7e4fcb345", + "sha256:eba2bc7c28a3b2b0a3c24caff33e4d8708db008f480b03a6ea39c28661663746", + "sha256:ee187977d587b9b81929e08022f385eb11274efd75795d59d99eb23b3fa9b055", + "sha256:f3ef7616ffc27c150ffec61ac820739495f6a9ca5d8532047102756ebb27e8d1", + "sha256:f46f223fcae09c8bf2746b4eb2f351294faae04b262429cc480d34c69b133fd9", + "sha256:fd9f6429a68a246fb466696d97d1240752c889b5bfdc219fea15ae787cf366a6" ], "index": "pypi", - "version": "==3.5.17" + "version": "==3.5.19" }, "requests": { "hashes": [ @@ -466,10 +466,10 @@ }, "shodan": { "hashes": [ - "sha256:f93b7199e89eecf5c84647f66316c2c044c3aebfc1fe4d9caa43dfda07f74c4e" + "sha256:c30baebce853ad67677bf002dde96a1ca1a9729bdd300fbb3c5e5d889547a639" ], "index": "pypi", - "version": "==1.11.1" + "version": "==1.12.1" }, "sigmatools": { "hashes": [ @@ -487,10 +487,10 @@ }, "soupsieve": { "hashes": [ - "sha256:3aef141566afd07201b525c17bfaadd07580a8066f82b57f7c9417f26adbd0a3", - "sha256:e41a65e99bd125972d84221022beb1e4b5cfc68fa12c170c39834ce32d1b294c" + "sha256:6898e82ecb03772a0d82bd0d0a10c0d6dcc342f77e0701d0ec4a8271be465ece", + "sha256:b20eff5e564529711544066d7dc0f7661df41232ae263619dede5059799cdfca" ], - "version": "==1.9" + "version": "==1.9.1" }, "sparqlwrapper": { "hashes": [ @@ -555,12 +555,12 @@ }, "vulners": { "hashes": [ - "sha256:6617d5904b5369507bc34105071d312e9e1c38d73654505e7b15b9a3f1325915", - "sha256:8b05d12a9dd7cbc07198a13281299a6e014ec348522e214b1efd097e194b7568", - "sha256:a19b02e0a112d70951e10c5abc1993f7f029234212828e1b617ab35f4e460a24" + "sha256:6506f3ad45bf3fd72f9cd1ebd5fbc13f814e7cf62faed6897e33db949fe4584a", + "sha256:a0e86015343ecf1c3313f6101567749988b5eb5299a672f19fd2974121817444", + "sha256:f243fe025a84b85bd6f37e45d6cf693e24697d30661695fe5d29b652dff6a5a1" ], "index": "pypi", - "version": "==1.4.7" + "version": "==1.4.9" }, "wand": { "hashes": [ @@ -572,10 +572,10 @@ }, "xlsxwriter": { "hashes": [ - "sha256:de9ef46088489915eaaee00c7088cff93cf613e9990b46b933c98eb46f21b47f", - "sha256:df96eafc3136d9e790e35d6725b473e46ada6f585c1f6519da69b27f5c8873f7" + "sha256:3a4e4a24a6753f046dc5a5e5bc5f443fce6a18988486885a258db6963eb54163", + "sha256:92a2ba339ca939815f0e125fcde728e94ccdb3e97e1acd3275ecf25a3cacfdc6" ], - "version": "==1.1.5" + "version": "==1.1.6" }, "yara-python": { "hashes": [ @@ -760,11 +760,11 @@ }, "pytest": { "hashes": [ - "sha256:13c5e9fb5ec5179995e9357111ab089af350d788cbc944c628f3cde72285809b", - "sha256:f21d2f1fb8200830dcbb5d8ec466a9c9120e20d8b53c7585d180125cce1d297a" + "sha256:3773f4c235918987d51daf1db66d51c99fac654c81d6f2f709a046ab446d5e5d", + "sha256:b7802283b70ca24d7119b32915efa7c409982f59913c1a6c0640aacf118b95f5" ], "index": "pypi", - "version": "==4.4.0" + "version": "==4.4.1" }, "requests": { "hashes": [ From eefa35c65db9915af7e7a47a6ca4ba80ac29742a Mon Sep 17 00:00:00 2001 From: Evert0x <35029353+Evert0x@users.noreply.github.com> Date: Thu, 18 Apr 2019 00:23:38 +0200 Subject: [PATCH 068/207] Create cuckoo_submit.py --- .../modules/expansion/cuckoo_submit.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 misp_modules/modules/expansion/cuckoo_submit.py diff --git a/misp_modules/modules/expansion/cuckoo_submit.py b/misp_modules/modules/expansion/cuckoo_submit.py new file mode 100644 index 0000000..d0cb236 --- /dev/null +++ b/misp_modules/modules/expansion/cuckoo_submit.py @@ -0,0 +1,62 @@ +import json +import requests + +misperrors = {'error': 'Error'} +mispattributes = { + 'input': ['url'], + 'output': ['text'] +} + +# possible module-types: 'expansion', 'hover' or both +moduleinfo = {'version': '1', 'author': 'Evert Kors', + 'description': 'MODULE_DESCRIPTION', + 'module-type': ['expansion', 'hover']} + +# config fields that your code expects from the site admin +moduleconfig = ['cuckoo_api'] + + +def handler(q=False): + if q is False: + return False + request = json.loads(q) + config = request.get('config') + if config is None: + misperrors['error'] = 'config is missing' + return misperrors + + cuck = config.get('cuckoo_api') + if cuck is None: + misperrors['error'] = 'cuckoo api url is missing' + return misperrors + + # The url to submit + url = request.get('url') + + HEADERS = {"Authorization": "Bearer S4MPL3"} + + urls = [ + url + ] + + try: + r = requests.post( + "%s/tasks/create/submit" % (cuck), + headers=HEADERS, + data={"strings": "\n".join(urls)} + ) + except Exception as e: + misperrors['error'] = str(e) + return misperrors + + r = {'results': [{'types': "text", 'values': "cool"}]} + return r + + +def introspection(): + return mispattributes + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo From e243edb50341899a4feedbd90ec3bd4e5df5fd00 Mon Sep 17 00:00:00 2001 From: Evert0x <35029353+Evert0x@users.noreply.github.com> Date: Thu, 18 Apr 2019 14:25:05 +0200 Subject: [PATCH 069/207] Update __init__.py --- misp_modules/modules/expansion/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/__init__.py b/misp_modules/modules/expansion/__init__.py index 42f74a3..df83ca9 100644 --- a/misp_modules/modules/expansion/__init__.py +++ b/misp_modules/modules/expansion/__init__.py @@ -1,6 +1,6 @@ from . import _vmray # noqa -__all__ = ['vmray_submit', 'bgpranking', 'circl_passivedns', 'circl_passivessl', +__all__ = ['cuckoo_submit', 'vmray_submit', 'bgpranking', 'circl_passivedns', 'circl_passivessl', 'countrycode', 'cve', 'dns', 'btc_steroids', 'domaintools', 'eupi', 'farsight_passivedns', 'ipasn', 'passivetotal', 'sourcecache', 'virustotal', 'whois', 'shodan', 'reversedns', 'geoip_country', 'wiki', 'iprep', From 49acb5374538a4fe1bafbb4af44d422f8ca0287d Mon Sep 17 00:00:00 2001 From: Ricardo van Zutphen Date: Fri, 19 Apr 2019 14:06:35 +0200 Subject: [PATCH 070/207] Update Cuckoo module to support files and URLs --- .../modules/expansion/cuckoo_submit.py | 151 ++++++++++++++---- 1 file changed, 119 insertions(+), 32 deletions(-) diff --git a/misp_modules/modules/expansion/cuckoo_submit.py b/misp_modules/modules/expansion/cuckoo_submit.py index d0cb236..e368fbd 100644 --- a/misp_modules/modules/expansion/cuckoo_submit.py +++ b/misp_modules/modules/expansion/cuckoo_submit.py @@ -1,56 +1,143 @@ +import base64 +import io import json +import logging import requests +import sys +import urllib.parse +import zipfile -misperrors = {'error': 'Error'} +from requests.exceptions import RequestException + +log = logging.getLogger('cuckoo_submit') +log.setLevel(logging.DEBUG) +sh = logging.StreamHandler(sys.stdout) +sh.setLevel(logging.DEBUG) +fmt = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +sh.setFormatter(fmt) +log.addHandler(sh) + +moduleinfo = { + "version": "0.1", 'author': "Evert Kors", + "description": "Submit files and URLs to Cuckoo Sandbox", + "module-type": ["expansion", "hover"] +} +misperrors = {"error": "Error"} +moduleconfig = ["cuckoo_api", "api_key"] mispattributes = { - 'input': ['url'], - 'output': ['text'] + "input": ["attachment', 'malware-sample", "url", "domain"], + "output": ["text"] } -# possible module-types: 'expansion', 'hover' or both -moduleinfo = {'version': '1', 'author': 'Evert Kors', - 'description': 'MODULE_DESCRIPTION', - 'module-type': ['expansion', 'hover']} -# config fields that your code expects from the site admin -moduleconfig = ['cuckoo_api'] +class APIKeyError(RequestException): + """Raised if the Cuckoo API returns a 401. This means no or an invalid + bearer token was supplied.""" + pass + + +class CuckooAPI(object): + + def __init__(self, api_url, api_key=""): + self.api_key = api_key + if not api_url.startswith("http"): + api_url = "https://{}".format(api_url) + + self.api_url = api_url + + def _post_api(self, endpoint, files=None, data={}): + data.update({ + "owner": "MISP" + }) + + try: + response = requests.post( + urllib.parse.urljoin(self.api_url, endpoint), + files=files, data=data, + headers={"Authorization: Bearer {}".format(self.api_key)} + ) + except RequestException as e: + log.error("Failed to submit sample to Cuckoo Sandbox. %s", e) + return None + + if response.status_code == 401: + raise APIKeyError("Invalid or no Cuckoo Sandbox API key provided") + + return response.json() + + def create_task(self, filename, fp): + response = self._post_api( + "/tasks/create/file", files={"file": (filename, fp)} + ) + if not response: + return False + + return response["task_id"] + + def create_url(self, url): + response = self._post_api( + "/tasks/create/url", data={"url": url} + ) + if not response: + return False + + return response["task_id"] def handler(q=False): if q is False: return False + request = json.loads(q) - config = request.get('config') - if config is None: - misperrors['error'] = 'config is missing' + + # See if the API URL was provided. The API key is optional, as it can + # be disabled in the Cuckoo API settings. + api_url = request["config"].get("api_url") + api_key = request["config"].get("api_key", "") + if not api_url: + misperrors["error"] = "No Cuckoo API URL provided" return misperrors - cuck = config.get('cuckoo_api') - if cuck is None: - misperrors['error'] = 'cuckoo api url is missing' - return misperrors + url = request.get("url") or request.get("domain") + data = request.get("data") + filename = None + if data: + data = base64.b64decode(data) - # The url to submit - url = request.get('url') + if "malware-sample" in request: + filename = request.get("malware-sample").split("|", 1)[0] + with zipfile.ZipFile(io.BytesIO(data)) as zipf: + data = zipf.read(zipf.namelist()[0], pwd=b"infected") - HEADERS = {"Authorization": "Bearer S4MPL3"} - - urls = [ - url - ] + elif "attachment" in request: + filename = request.get("attachment") + cuckoo_api = CuckooAPI(api_url=api_url, api_key=api_key) + task_id = None try: - r = requests.post( - "%s/tasks/create/submit" % (cuck), - headers=HEADERS, - data={"strings": "\n".join(urls)} - ) - except Exception as e: - misperrors['error'] = str(e) + if url: + log.debug("Submitting URL to Cuckoo Sandbox %s", api_url) + task_id = cuckoo_api.create_url(url) + elif data and filename: + log.debug("Submitting file to Cuckoo Sandbox %s", api_url) + task_id = cuckoo_api.create_task( + filename=filename, fp=io.BytesIO(data) + ) + except APIKeyError as e: + misperrors["error"] = "Failed to submit to Cuckoo: {}".format(e) return misperrors - r = {'results': [{'types': "text", 'values': "cool"}]} - return r + if not task_id: + misperrors["error"] = "File or URL submission failed" + return misperrors + + return { + "results": [ + {"types": "text", "values": "Cuckoo task id: {}".format(task_id)} + ] + } def introspection(): From e6326185d568575a200147a2866aa8cd9769ac28 Mon Sep 17 00:00:00 2001 From: Ricardo van Zutphen Date: Fri, 19 Apr 2019 16:24:30 +0200 Subject: [PATCH 071/207] Use double quotes and provide headers correctly --- .../modules/expansion/cuckoo_submit.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/misp_modules/modules/expansion/cuckoo_submit.py b/misp_modules/modules/expansion/cuckoo_submit.py index e368fbd..c1ded90 100644 --- a/misp_modules/modules/expansion/cuckoo_submit.py +++ b/misp_modules/modules/expansion/cuckoo_submit.py @@ -9,25 +9,25 @@ import zipfile from requests.exceptions import RequestException -log = logging.getLogger('cuckoo_submit') +log = logging.getLogger("cuckoo_submit") log.setLevel(logging.DEBUG) sh = logging.StreamHandler(sys.stdout) sh.setLevel(logging.DEBUG) fmt = logging.Formatter( - '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) sh.setFormatter(fmt) log.addHandler(sh) moduleinfo = { - "version": "0.1", 'author': "Evert Kors", + "version": "0.1", "author": "Evert Kors", "description": "Submit files and URLs to Cuckoo Sandbox", "module-type": ["expansion", "hover"] } misperrors = {"error": "Error"} -moduleconfig = ["cuckoo_api", "api_key"] +moduleconfig = ["api_url", "api_key"] mispattributes = { - "input": ["attachment', 'malware-sample", "url", "domain"], + "input": ["attachment", "malware-sample", "url", "domain"], "output": ["text"] } @@ -56,7 +56,7 @@ class CuckooAPI(object): response = requests.post( urllib.parse.urljoin(self.api_url, endpoint), files=files, data=data, - headers={"Authorization: Bearer {}".format(self.api_key)} + headers={"Authorization": "Bearer {}".format(self.api_key)} ) except RequestException as e: log.error("Failed to submit sample to Cuckoo Sandbox. %s", e) @@ -65,6 +65,10 @@ class CuckooAPI(object): if response.status_code == 401: raise APIKeyError("Invalid or no Cuckoo Sandbox API key provided") + if response.status_code != 200: + log.error("Invalid Cuckoo API response") + return None + return response.json() def create_task(self, filename, fp): @@ -145,5 +149,5 @@ def introspection(): def version(): - moduleinfo['config'] = moduleconfig + moduleinfo["config"] = moduleconfig return moduleinfo From 7fefbd2a4c074c43b73d803f6c87491aa564996c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Fri, 19 Apr 2019 22:41:06 +0200 Subject: [PATCH 072/207] chg: Bump dependencies Fix CVE-2019-11324 (urllib3) --- Pipfile.lock | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 428ae11..4a5c092 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -300,7 +300,7 @@ "pybgpranking": { "editable": true, "git": "https://github.com/D4-project/BGP-Ranking.git/", - "ref": "86180c080ba5f83a5160a8295ebc263709aa4eba", + "ref": "4e0741056bcc0077de1120b8724a31330b26033e", "subdirectory": "client" }, "pydnstrails": { @@ -331,13 +331,13 @@ "pyipasnhistory": { "editable": true, "git": "https://github.com/D4-project/IPASN-History.git/", - "ref": "1009124c39e9cc3b362f0f9ce4d272817b9a5186", + "ref": "c0c2bbf8d70811982dad065ea463a7e01593a38d", "subdirectory": "client" }, "pymisp": { "editable": true, "git": "https://github.com/MISP/PyMISP.git", - "ref": "13445b51748524195bb15aec2c1c6176bcb6bd95" + "ref": "921f414e0e026a3a4b77112012cf930242a33b04" }, "pyonyphe": { "editable": true, @@ -459,10 +459,10 @@ }, "requests-cache": { "hashes": [ - "sha256:e9270030becc739b0a7f7f834234c73a878b2d794122bf76f40055a22419eb67", - "sha256:fe561ca119879bbcfb51f03a35e35b425e18f338248e59fd5cf2166c77f457a2" + "sha256:6822f788c5ee248995c4bfbd725de2002ad710182ba26a666e85b64981866060", + "sha256:73a7211870f7d67af5fd81cad2f67cfe1cd3eb4ee6a85155e07613968cc72dfc" ], - "version": "==0.4.13" + "version": "==0.5.0" }, "shodan": { "hashes": [ @@ -494,12 +494,12 @@ }, "sparqlwrapper": { "hashes": [ - "sha256:2a95fdede2833be660b81092934c4a0054ff85f2693098556762a2759ea486f1", - "sha256:7f4c8d38ea1bfcffbc358c9a05de35a3fd7152cc3e8ea57963ee7a0a242f7a5e", - "sha256:acf6d60f0a3684cb673653b07871acb0c350a974b891f20f8ac94926ff9eb2ff" + "sha256:14ec551f0d60b4a496ffcc31f15337e844c085b8ead8cbe9a7178748a6de3794", + "sha256:21928e7a97f565e772cdeeb0abad428960f4307e3a13dbdd8f6d3da8a6a506c9", + "sha256:abc3e7eadcad32fa69a85c003853e2f6f73bda6cc999853838f401a5a1ea1109" ], "index": "pypi", - "version": "==1.8.2" + "version": "==1.8.4" }, "stix2-patterns": { "hashes": [ @@ -542,10 +542,10 @@ }, "urllib3": { "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + "sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0", + "sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3" ], - "version": "==1.24.1" + "version": "==1.24.2" }, "uwhois": { "editable": true, @@ -783,10 +783,10 @@ }, "urllib3": { "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + "sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0", + "sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3" ], - "version": "==1.24.1" + "version": "==1.24.2" } } } From 5367bcd409e55eff39ecc498aa28ceab39988f6d Mon Sep 17 00:00:00 2001 From: Ricardo van Zutphen Date: Mon, 22 Apr 2019 22:38:03 +0200 Subject: [PATCH 073/207] Document Cuckoo expansion module --- doc/expansion/cuckoo_submit.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 doc/expansion/cuckoo_submit.json diff --git a/doc/expansion/cuckoo_submit.json b/doc/expansion/cuckoo_submit.json new file mode 100644 index 0000000..7fe8067 --- /dev/null +++ b/doc/expansion/cuckoo_submit.json @@ -0,0 +1,9 @@ +{ + "description": "An expansion module to submit files and URLs to Cuckoo Sandbox.", + "logo": "logos/cuckoo.png", + "requirements": ["Access to a Cuckoo Sandbox API and an API key if the API requires it. (api_url and api_key)"], + "input": "A malware-sample or attachment for files. A url or domain for URLs.", + "output": "A text field containing 'Cuckoo task id: '", + "references": ["https://cuckoosandbox.org/", "https://cuckoo.sh/docs/"], + "features": "The module takes a malware-sample, attachment, url or domain and submits it to Cuckoo Sandbox.\n The returned task id can be used to retrieve results when the analysis completed." +} From cafa1a6229649358ac04e2d1326bc07747628046 Mon Sep 17 00:00:00 2001 From: Ricardo van Zutphen Date: Mon, 22 Apr 2019 22:45:38 +0200 Subject: [PATCH 074/207] Generate latest version of documentation --- doc/README.md | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/doc/README.md b/doc/README.md index c32a8c4..02506ab 100644 --- a/doc/README.md +++ b/doc/README.md @@ -178,6 +178,25 @@ Module to query Crowdstrike Falcon. ----- +#### [cuckoo_submit](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/cuckoo_submit.py) + + + +An expansion module to submit files and URLs to Cuckoo Sandbox. +- **features**: +>The module takes a malware-sample, attachment, url or domain and submits it to Cuckoo Sandbox. +> The returned task id can be used to retrieve results when the analysis completed. +- **input**: +>A malware-sample or attachment for files. A url or domain for URLs. +- **output**: +>A text field containing 'Cuckoo task id: ' +- **references**: +>https://cuckoosandbox.org/, https://cuckoo.sh/docs/ +- **requirements**: +>Access to a Cuckoo Sandbox API and an API key if the API requires it. (api_url and api_key) + +----- + #### [cve](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/cve.py) @@ -1081,7 +1100,13 @@ OSQuery export of a MISP event. Simple export of a MISP event to PDF. - **features**: ->The module takes care of the PDF file building, and work with any MISP Event. Except the requirement of asciidoctor, used to create the file, there is no special feature concerning the Event. +>The module takes care of the PDF file building, and work with any MISP Event. Except the requirement of reportlab, used to create the file, there is no special feature concerning the Event. Some parameters can be given through the config dict. 'MISP_base_url_for_dynamic_link' is your MISP URL, to attach an hyperlink to your event on your MISP instance from the PDF. Keep it clear to avoid hyperlinks in the generated pdf. +> 'MISP_name_for_metadata' is your CERT or MISP instance name. Used as text in the PDF' metadata +> 'Activate_textual_description' is a boolean (True or void) to activate the textual description/header abstract of an event +> 'Activate_galaxy_description' is a boolean (True or void) to activate the description of event related galaxies. +> 'Activate_related_events' is a boolean (True or void) to activate the description of related event. Be aware this might leak information on confidential events linked to the current event ! +> 'Activate_internationalization_fonts' is a boolean (True or void) to activate Noto fonts instead of default fonts (Helvetica). This allows the support of CJK alphabet. Be sure to have followed the procedure to download Noto fonts (~70Mo) in the right place (/tools/pdf_fonts/Noto_TTF), to allow PyMisp to find and use them during PDF generation. +> 'Custom_fonts_path' is a text (path or void) to the TTF file of your choice, to create the PDF with it. Be aware the PDF won't support bold/italic/special style anymore with this option - **input**: >MISP Event - **output**: @@ -1089,7 +1114,7 @@ Simple export of a MISP event to PDF. - **references**: >https://acrobat.adobe.com/us/en/acrobat/about-adobe-pdf.html - **requirements**: ->PyMISP, asciidoctor +>PyMISP, reportlab ----- From c85ab8d93ca9485e5534cd779cc91e5ba8c9d2c8 Mon Sep 17 00:00:00 2001 From: Sascha Rommelfangen Date: Tue, 23 Apr 2019 11:38:56 +0200 Subject: [PATCH 075/207] initial version of QR code reader Module accepts attachments and processes pictures. It tries to identify and analyze an existing QR code. Identified values can be inserted into the event. --- misp_modules/modules/expansion/qrcode.py | 86 ++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 misp_modules/modules/expansion/qrcode.py diff --git a/misp_modules/modules/expansion/qrcode.py b/misp_modules/modules/expansion/qrcode.py new file mode 100644 index 0000000..5357e6d --- /dev/null +++ b/misp_modules/modules/expansion/qrcode.py @@ -0,0 +1,86 @@ +import json +from pyzbar import pyzbar +import cv2 +import re +import binascii +import np + +misperrors = {'error': 'Error'} +mispattributes = {'input': ['attachment'], + 'output': ['url', 'btc']} +moduleinfo = {'version': '0.1', 'author': 'Sascha Rommelfangen', + 'description': 'QR code decoder', + 'module-type': ['expansion', 'hover']} + +debug = True +debug_prefix = "[DEBUG] QR Code module: " +# format example: bitcoin:1GXZ6v7FZzYBEnoRaG77SJxhu7QkvQmFuh?amount=0.15424 +# format example: http://example.com +cryptocurrencies = {'bitcoin'} +schemas = {'http://', 'https://', 'ftp://'} +moduleconfig = [] + +def handler(q=False): + if q is False: + return False + q = json.loads(q) + filename = q['attachment'] + try: + img_array = np.fromstring(binascii.a2b_base64(q['data']), np.uint8) + except: + err = "Couldn't fetch attachment (JSON 'data' is empty). Are you using the 'Query enrichment' action?" + misperrors['error'] = err + print(err) + return misperrors + image = cv2.imdecode(img_array, cv2.IMREAD_COLOR) + if q: + barcodes = pyzbar.decode(image) + for item in barcodes: + try: + result = item.data.decode() + except Exception as e: + print(e) + return + if debug: + print(debug_prefix + result) + for item in cryptocurrencies: + if item in result: + try: + currency, address, extra = re.split('\:|\?', result) + except Exception as e: + print(e) + if currency in cryptocurrencies: + try: + amount = re.split('=', extra)[1] + if debug: + print(debug_prefix + address) + print(debug_prefix + amount) + return {'results': [{'types': ['btc'], 'values': address, 'comment': "BTC: " + amount + " from file " + filename}]} + except Exception as e: + print(e) + else: + print(address) + for item in schemas: + if item in result: + try: + url = result + if debug: + print(debug_prefix + url) + return {'results': [{'types': ['url'], 'values': url, 'comment': "from QR code of file " + filename}]} + except Exception as e: + print(e) + else: + try: + return {'results': [{'types': ['text'], 'values': result, 'comment': "from QR code of file " + filename}]} + except Exception as e: + print(e) + misperrors['error'] = "Couldn't decode QR code in attachment." + return misperrors + +def introspection(): + return mispattributes + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo From d5180e7e79584cf256ad88db1422f3b155445122 Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Tue, 23 Apr 2019 14:37:27 +0200 Subject: [PATCH 076/207] chg: [qrcode] various fixes to make it PEP compliant --- misp_modules/modules/expansion/qrcode.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/misp_modules/modules/expansion/qrcode.py b/misp_modules/modules/expansion/qrcode.py index 5357e6d..82f4f88 100644 --- a/misp_modules/modules/expansion/qrcode.py +++ b/misp_modules/modules/expansion/qrcode.py @@ -20,6 +20,7 @@ cryptocurrencies = {'bitcoin'} schemas = {'http://', 'https://', 'ftp://'} moduleconfig = [] + def handler(q=False): if q is False: return False @@ -27,7 +28,7 @@ def handler(q=False): filename = q['attachment'] try: img_array = np.fromstring(binascii.a2b_base64(q['data']), np.uint8) - except: + except Exception as e: err = "Couldn't fetch attachment (JSON 'data' is empty). Are you using the 'Query enrichment' action?" misperrors['error'] = err print(err) @@ -46,7 +47,7 @@ def handler(q=False): for item in cryptocurrencies: if item in result: try: - currency, address, extra = re.split('\:|\?', result) + currency, address, extra = re.split('\:|\?', result) except Exception as e: print(e) if currency in cryptocurrencies: @@ -60,7 +61,7 @@ def handler(q=False): print(e) else: print(address) - for item in schemas: + for item in schemas: if item in result: try: url = result @@ -77,6 +78,7 @@ def handler(q=False): misperrors['error'] = "Couldn't decode QR code in attachment." return misperrors + def introspection(): return mispattributes From 44050ec4da5b830161b2f9397e7910309cf1bdd5 Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Tue, 23 Apr 2019 14:44:00 +0200 Subject: [PATCH 077/207] chg: [qrcode] flake8 needs some drugs --- misp_modules/modules/expansion/qrcode.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/qrcode.py b/misp_modules/modules/expansion/qrcode.py index 82f4f88..437de60 100644 --- a/misp_modules/modules/expansion/qrcode.py +++ b/misp_modules/modules/expansion/qrcode.py @@ -32,6 +32,7 @@ def handler(q=False): err = "Couldn't fetch attachment (JSON 'data' is empty). Are you using the 'Query enrichment' action?" misperrors['error'] = err print(err) + print(e) return misperrors image = cv2.imdecode(img_array, cv2.IMREAD_COLOR) if q: @@ -47,7 +48,7 @@ def handler(q=False): for item in cryptocurrencies: if item in result: try: - currency, address, extra = re.split('\:|\?', result) + currency, address, extra = re.split(r'\:|\?', result) except Exception as e: print(e) if currency in cryptocurrencies: From e55ae11a1e126ad040eef1e17ae6836c770f2629 Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Tue, 23 Apr 2019 14:45:12 +0200 Subject: [PATCH 078/207] chg: [qrcode] added to the __init__ --- misp_modules/modules/expansion/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/__init__.py b/misp_modules/modules/expansion/__init__.py index 42f74a3..ff847a4 100644 --- a/misp_modules/modules/expansion/__init__.py +++ b/misp_modules/modules/expansion/__init__.py @@ -8,4 +8,4 @@ __all__ = ['vmray_submit', 'bgpranking', 'circl_passivedns', 'circl_passivessl', 'yara_syntax_validator', 'hashdd', 'onyphe', 'onyphe_full', 'rbl', 'xforceexchange', 'sigma_syntax_validator', 'stix2_pattern_syntax_validator', 'sigma_queries', 'dbl_spamhaus', 'vulners', 'yara_query', 'macaddress_io', - 'intel471', 'backscatter_io', 'btc_scam_check', 'hibp', 'greynoise', 'macvendors'] + 'intel471', 'backscatter_io', 'btc_scam_check', 'hibp', 'greynoise', 'macvendors', 'qrcode'] From 32430a15cb1794bb41596e5a4b80a2f2fce77109 Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Tue, 23 Apr 2019 14:49:02 +0200 Subject: [PATCH 079/207] chg: [qrcode] add requirements --- Pipfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Pipfile b/Pipfile index 2f2d172..6a88fcc 100644 --- a/Pipfile +++ b/Pipfile @@ -42,6 +42,9 @@ misp-modules = {editable = true,path = "."} pybgpranking = {editable = true,git = "https://github.com/D4-project/BGP-Ranking.git/",subdirectory = "client"} pyipasnhistory = {editable = true,git = "https://github.com/D4-project/IPASN-History.git/",subdirectory = "client"} backscatter = "*" +pyzbar = "*" +opencv-python = "*" +np = "*" [requires] python_version = "3.6" From 5adb9bfcfab220f5db5af4f0ec89a9611984db8b Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Tue, 23 Apr 2019 14:54:05 +0200 Subject: [PATCH 080/207] chg: [doc] qrcode and Cisco FireSight added --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 18b5548..94a5778 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ * [onyphe_full](misp_modules/modules/expansion/onyphe_full.py) - a modules to process full queries on Onyphe. * [OTX](misp_modules/modules/expansion/otx.py) - an expansion module for [OTX](https://otx.alienvault.com/). * [passivetotal](misp_modules/modules/expansion/passivetotal.py) - a [passivetotal](https://www.passivetotal.org/) module that queries a number of different PassiveTotal datasets. +* [qrcode](misp_modules/modules/expansion/qrcode.py) - a module decode QR code, barcode and similar codes from an image and enrich with the decoded values. * [rbl](misp_modules/modules/expansion/rbl.py) - a module to get RBL (Real-Time Blackhost List) values from an attribute. * [reversedns](misp_modules/modules/expansion/reversedns.py) - Simple Reverse DNS expansion service to resolve reverse DNS from MISP attributes. * [securitytrails](misp_modules/modules/expansion/securitytrails.py) - an expansion module for [securitytrails](https://securitytrails.com/). @@ -68,6 +69,7 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ ### Export modules * [CEF](misp_modules/modules/export_mod/cef_export.py) module to export Common Event Format (CEF). +* [Cisco FireSight Manager ACL rule](misp_modules/modules/export_mod/cisco_firesight_manager_ACL_rule_export.py) module to export as rule for the Cisco FireSight manager ACL. * [GoAML export](misp_modules/modules/export_mod/goamlexport.py) module to export in [GoAML format](http://goaml.unodc.org/goaml/en/index.html). * [Lite Export](misp_modules/modules/export_mod/liteexport.py) module to export a lite event. * [PDF export](misp_modules/modules/export_mod/pdfexport.py) module to export an event in PDF. From 8acbb1762d07669d41ab9215f563507535a288c1 Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Tue, 23 Apr 2019 14:59:42 +0200 Subject: [PATCH 081/207] chg: [travis] because everyone need a bar --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index e8fea8e..b0d73ed 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ python: - "3.7-dev" install: + - apt-get install libzbar0 libzbar-dev - pip install pipenv - pipenv install --dev From 72cd5e3c1f0ccfae85e30f3644cef578d1239f06 Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Tue, 23 Apr 2019 15:02:32 +0200 Subject: [PATCH 082/207] chg: [travis] because we all need sudo --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b0d73ed..52a7742 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ python: - "3.7-dev" install: - - apt-get install libzbar0 libzbar-dev + - sudo apt-get install libzbar0 libzbar-dev - pip install pipenv - pipenv install --dev From 2d8aaf09c20849b1ce6f251dfe07417341b0d29a Mon Sep 17 00:00:00 2001 From: Sascha Rommelfangen Date: Tue, 23 Apr 2019 15:40:22 +0200 Subject: [PATCH 083/207] brackets are difficult... --- misp_modules/modules/expansion/qrcode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/misp_modules/modules/expansion/qrcode.py b/misp_modules/modules/expansion/qrcode.py index 437de60..9a62827 100644 --- a/misp_modules/modules/expansion/qrcode.py +++ b/misp_modules/modules/expansion/qrcode.py @@ -16,8 +16,8 @@ debug = True debug_prefix = "[DEBUG] QR Code module: " # format example: bitcoin:1GXZ6v7FZzYBEnoRaG77SJxhu7QkvQmFuh?amount=0.15424 # format example: http://example.com -cryptocurrencies = {'bitcoin'} -schemas = {'http://', 'https://', 'ftp://'} +cryptocurrencies = ['bitcoin'] +schemas = ['http://', 'https://', 'ftp://'] moduleconfig = [] From b787aa7961976f560c638db9eff3223a6be07d29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Tue, 23 Apr 2019 17:02:21 +0200 Subject: [PATCH 084/207] chg: Require python3 instead of python 3.6 --- Pipfile | 2 +- Pipfile.lock | 99 ++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 90 insertions(+), 11 deletions(-) diff --git a/Pipfile b/Pipfile index 6a88fcc..ba650fe 100644 --- a/Pipfile +++ b/Pipfile @@ -47,4 +47,4 @@ opencv-python = "*" np = "*" [requires] -python_version = "3.6" +python_version = "3" diff --git a/Pipfile.lock b/Pipfile.lock index 4a5c092..3191d4b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "23dec0fa6400c828e294ea9981b433903c17358ca61d7abdaec8df5a1c89f08c" + "sha256": "7fee9399d8a7151a79b6f8bbce64564062fd562b0a091fd45a875884d3fb954e" }, "pipfile-spec": 6, "requires": { - "python_version": "3.6" + "python_version": "3" }, "sources": [ { @@ -158,9 +158,10 @@ }, "httplib2": { "hashes": [ - "sha256:4ba6b8fd77d0038769bf3c33c9a96a6f752bc4cdf739701fdcaf210121f399d4" + "sha256:23914b5487dfe8ef09db6656d6d63afb0cf3054ad9ebc50868ddc8e166b5f8e8", + "sha256:a18121c7c72a56689efbf1aef990139ad940fee1e64c6f2458831736cd593600" ], - "version": "==0.12.1" + "version": "==0.12.3" }, "idna": { "hashes": [ @@ -236,6 +237,41 @@ ], "version": "==4.5.2" }, + "np": { + "hashes": [ + "sha256:781265283f3823663ad8fb48741aae62abcf4c78bc19f908f8aa7c1d3eb132f8" + ], + "index": "pypi", + "version": "==1.0.2" + }, + "numpy": { + "hashes": [ + "sha256:0e2eed77804b2a6a88741f8fcac02c5499bba3953ec9c71e8b217fad4912c56c", + "sha256:1c666f04553ef70fda54adf097dbae7080645435fc273e2397f26bbf1d127bbb", + "sha256:1f46532afa7b2903bfb1b79becca2954c0a04389d19e03dc73f06b039048ac40", + "sha256:315fa1b1dfc16ae0f03f8fd1c55f23fd15368710f641d570236f3d78af55e340", + "sha256:3d5fcea4f5ed40c3280791d54da3ad2ecf896f4c87c877b113576b8280c59441", + "sha256:48241759b99d60aba63b0e590332c600fc4b46ad597c9b0a53f350b871ef0634", + "sha256:4b4f2924b36d857cf302aec369caac61e43500c17eeef0d7baacad1084c0ee84", + "sha256:54fe3b7ed9e7eb928bbc4318f954d133851865f062fa4bbb02ef8940bc67b5d2", + "sha256:5a8f021c70e6206c317974c93eaaf9bc2b56295b6b1cacccf88846e44a1f33fc", + "sha256:754a6be26d938e6ca91942804eb209307b73f806a1721176278a6038869a1686", + "sha256:771147e654e8b95eea1293174a94f34e2e77d5729ad44aefb62fbf8a79747a15", + "sha256:78a6f89da87eeb48014ec652a65c4ffde370c036d780a995edaeb121d3625621", + "sha256:7fde5c2a3a682a9e101e61d97696687ebdba47637611378b4127fe7e47fdf2bf", + "sha256:80d99399c97f646e873dd8ce87c38cfdbb668956bbc39bc1e6cac4b515bba2a0", + "sha256:88a72c1e45a0ae24d1f249a529d9f71fe82e6fa6a3fd61414b829396ec585900", + "sha256:a4f4460877a16ac73302a9c077ca545498d9fe64e6a81398d8e1a67e4695e3df", + "sha256:a61255a765b3ac73ee4b110b28fccfbf758c985677f526c2b4b39c48cc4b509d", + "sha256:ab4896a8c910b9a04c0142871d8800c76c8a2e5ff44763513e1dd9d9631ce897", + "sha256:abbd6b1c2ef6199f4b7ca9f818eb6b31f17b73a6110aadc4e4298c3f00fab24e", + "sha256:b16d88da290334e33ea992c56492326ea3b06233a00a1855414360b77ca72f26", + "sha256:b78a1defedb0e8f6ae1eb55fa6ac74ab42acc4569c3a2eacc2a407ee5d42ebcb", + "sha256:cfef82c43b8b29ca436560d51b2251d5117818a8d1fb74a8384a83c096745dad", + "sha256:d160e57731fcdec2beda807ebcabf39823c47e9409485b5a3a1db3a8c6ce763e" + ], + "version": "==1.16.3" + }, "oauth2": { "hashes": [ "sha256:15b5c42301f46dd63113f1214b0d81a8b16254f65a86d3c32a1b52297f3266e6", @@ -244,6 +280,39 @@ "index": "pypi", "version": "==1.9.0.post1" }, + "opencv-python": { + "hashes": [ + "sha256:1703a296a96d3d46615e5053f224867977accb4240bcaa0fcabcb0768bf5ac13", + "sha256:1777ce7535ee7a1995cae168a107a1320e9df13648b930e72a1a2c2eccd64cda", + "sha256:1e5520482fb18fbd64d079e7f17ac0018f195fd75f6360a53bb82d7903106b50", + "sha256:25522dcf2529614750a71112a6659759080b4bdc2323f19d47f4d895960fd796", + "sha256:2af5f2842ad44c65ae2647377e0ff198719e1a1cfc9c6a19bc0c525c035d4bd8", + "sha256:31ec48d7eca13fc25c287dea7cecab453976e372cad8f50d55c054a247efda21", + "sha256:47cf48ff5dbd554e9f58cc9e98cf0b5de3f6a971172612bffa06bc5fb79ce872", + "sha256:494f98366bb5d6c2ac7e50e6617139f353704fd97a6d12ec9d392e72817d5cb0", + "sha256:4a9845870739e640e3350a8d98d511c92c087fe3d66090e83be7bf94e0ac64f7", + "sha256:4ac29cc0847d948a6636899014e84e165c30cc8779d6218394d44363462a01ce", + "sha256:5857ace03b7854221abf8072462d306c2c2ce4e366190b21d90ee8ee8aaf5bb4", + "sha256:5b4a23d99d5a2874767034466f5a8fd37b9f93ac14955a01b1a208983c76b9ad", + "sha256:734d87a5021c037064beb62133e135e66c7128e401a63b8b842b809ae2093749", + "sha256:78005c1c5d15ef4e32e0f485557bd15b5b6d87f49c19db7fe3e9246a61ebe7e4", + "sha256:81ae2283225c5c52fc3d72debd4241c30ccff2bb922578bf7867f9851cce3acb", + "sha256:88dbf900f297fdae0f62b899d6a784d8868ec2135854c5f8a9abbad00a6f0c5b", + "sha256:8c98ea7b8d327a31cd6028782a06147d0e0329ae8e829e881fb5d02f7ed8aec9", + "sha256:937d4686fef6967921145290f5b50c01c00c5b5d3542a6519e8a85cd88448723", + "sha256:a057958c0e362b3c4f03b9af1cbdb6d5af035fd22ecd7fd794eba8fdeb049eb8", + "sha256:c41eab31fa2c641226c6187caa391a688d064c99f078d604574f1912296b771f", + "sha256:cf4f7e62d1f80d1fa85a1693a3500def5cde54b2b75212b3609e552e4c25acfb", + "sha256:d90d60143e18334330c149f293071c9f2f3c79c896f33dc4ec65099e58baaaa7", + "sha256:db3106b7ca86999a7bd1f2fcc93e49314e5e6e451356774e421a69428df5020b", + "sha256:dbaf264db56f4771dfac6624f438bc4dc670aa94f61a6138848fcab7e9e77380", + "sha256:e65206c4cf651dc9cf0829962fae8bec986767c9f123d6a1ad17f9356bf7257e", + "sha256:eac94ddc78c58e891cff7180274317dad2938a4ddfc6ced1c04846c7f50e77e9", + "sha256:f2e828711f044a965509c862b3a59b3181e9c56c145a950cb53d43fec54e66d2" + ], + "index": "pypi", + "version": "==4.1.0.25" + }, "passivetotal": { "hashes": [ "sha256:d745a6519ec04e3a354682978ebf07778bf7602beac30307cbad075ff1a4418d" @@ -401,6 +470,15 @@ ], "version": "==5.1" }, + "pyzbar": { + "hashes": [ + "sha256:0e204b904e093e5e75aa85e0203bb0e02888105732a509b51f31cff400f34265", + "sha256:496249b546be70ec98c0ff0ad9151e73daaffff129266df86150a15dcd8dac4c", + "sha256:7d6c01d2c0a352fa994aa91b5540d1caeaeaac466656eb41468ca5df33be9f2e" + ], + "index": "pypi", + "version": "==0.1.8" + }, "rdflib": { "hashes": [ "sha256:58d5994610105a457cff7fdfe3d683d87786c5028a45ae032982498a7e913d6f", @@ -564,18 +642,19 @@ }, "wand": { "hashes": [ - "sha256:91810d241ab0851d40e67c946beb960b869c4f4160c397eac291ec6283ee3e3f", - "sha256:ae7c0958509a22f531b7b97e93adfd3f1208f0ac1c593af9e5f0cffa4ac06d5b" + "sha256:63ab24dee0264a44f5f045d4ecc0d392bc1cc195e5a2f80ce537b2c205c3033b", + "sha256:a2c318993791fab4fcfd460045415176f81d42f8c6fd8a88fb8d74d2f0f34b97", + "sha256:f68f32f2e4eca663a361d36148f06372de560442dcf8c785a53a64ee282572c9" ], "index": "pypi", - "version": "==0.5.2" + "version": "==0.5.3" }, "xlsxwriter": { "hashes": [ - "sha256:3a4e4a24a6753f046dc5a5e5bc5f443fce6a18988486885a258db6963eb54163", - "sha256:92a2ba339ca939815f0e125fcde728e94ccdb3e97e1acd3275ecf25a3cacfdc6" + "sha256:2a40b427dac0f640031e5b33abe97e761de6e0f12d4d346e7b2e2b67cf6ee927", + "sha256:431edc9ba1132eec1996939aa83fffe41885d3042ab09d47c3086f41a156c430" ], - "version": "==1.1.6" + "version": "==1.1.7" }, "yara-python": { "hashes": [ From 4631c17286dacc52c71eda700a26a2403162b85b Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Tue, 23 Apr 2019 19:49:58 +0200 Subject: [PATCH 085/207] chg: [doc] cuckoo_submit module added --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 94a5778..081120d 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ * [countrycode](misp_modules/modules/expansion/countrycode.py) - a hover module to tell you what country a URL belongs to. * [CrowdStrike Falcon](misp_modules/modules/expansion/crowdstrike_falcon.py) - an expansion module to expand using CrowdStrike Falcon Intel Indicator API. * [CVE](misp_modules/modules/expansion/cve.py) - a hover module to give more information about a vulnerability (CVE). +* [Cuckoo submit](misp_modules/modules/expansion/cuckoo_submit.py) - A hover module to submit malware sample, url, attachment, domain to Cuckoo Sandbox. * [DBL Spamhaus](misp_modules/modules/expansion/dbl_spamhaus.py) - a hover module to check Spamhaus DBL for a domain name. * [DNS](misp_modules/modules/expansion/dns.py) - a simple module to resolve MISP attributes like hostname and domain to expand IP addresses attributes. * [DomainTools](misp_modules/modules/expansion/domaintools.py) - a hover and expansion module to get information from [DomainTools](http://www.domaintools.com/) whois. From e893a17583a36c84d8dd6deb112eaf8c41476eb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Wed, 24 Apr 2019 11:29:16 +0200 Subject: [PATCH 086/207] chg: Bump dependencies, update REQUIREMENTS file --- Pipfile.lock | 8 ++++---- REQUIREMENTS | 47 +++++++++++++++++++++++++---------------------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 3191d4b..8a073a9 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -633,12 +633,12 @@ }, "vulners": { "hashes": [ - "sha256:6506f3ad45bf3fd72f9cd1ebd5fbc13f814e7cf62faed6897e33db949fe4584a", - "sha256:a0e86015343ecf1c3313f6101567749988b5eb5299a672f19fd2974121817444", - "sha256:f243fe025a84b85bd6f37e45d6cf693e24697d30661695fe5d29b652dff6a5a1" + "sha256:146ef130f215b50cdff790b06b4886c7edb325c075e9fce4bf1d3ab8d64a10d0", + "sha256:53406a86126159eaee9575fa667c99459bfdf9dd8c06bd0ce73fbe536b305e30", + "sha256:a258ccdbaee586207bc80d3590f0315ff151cfe16ea54f2e1629a6018fd9f2a3" ], "index": "pypi", - "version": "==1.4.9" + "version": "==1.5.0" }, "wand": { "hashes": [ diff --git a/REQUIREMENTS b/REQUIREMENTS index 99e1c02..b9d198a 100644 --- a/REQUIREMENTS +++ b/REQUIREMENTS @@ -1,9 +1,9 @@ -i https://pypi.org/simple -e . --e git+https://github.com/D4-project/BGP-Ranking.git/@37c97ae252ec4bf1d67733a49d4895c8cb009cf9#egg=pybgpranking&subdirectory=client --e git+https://github.com/D4-project/IPASN-History.git/@e846cd36fe1ed6b22f60890bba89f84e61b62e59#egg=pyipasnhistory&subdirectory=client +-e git+https://github.com/D4-project/BGP-Ranking.git/@4e0741056bcc0077de1120b8724a31330b26033e#egg=pybgpranking&subdirectory=client +-e git+https://github.com/D4-project/IPASN-History.git/@c0c2bbf8d70811982dad065ea463a7e01593a38d#egg=pyipasnhistory&subdirectory=client -e git+https://github.com/MISP/PyIntel471.git@0df8d51f1c1425de66714b3a5a45edb69b8cc2fc#egg=pyintel471 --e git+https://github.com/MISP/PyMISP.git@b8759673b91e733c307698abdc0d5ed82fd7e0de#egg=pymisp +-e git+https://github.com/MISP/PyMISP.git@921f414e0e026a3a4b77112012cf930242a33b04#egg=pymisp -e git+https://github.com/Rafiot/uwhoisd.git@411572840eba4c72dc321c549b36a54ed5cea9de#egg=uwhois&subdirectory=client -e git+https://github.com/sebdraven/pydnstrails@48c1f740025c51289f43a24863d1845ff12fd21a#egg=pydnstrails -e git+https://github.com/sebdraven/pyonyphe@cbb0168d5cb28a9f71f7ab3773164a7039ccdb12#egg=pyonyphe @@ -16,7 +16,7 @@ beautifulsoup4==4.7.1 blockchain==1.4.4 certifi==2019.3.9 chardet==3.0.4 -click-plugins==1.0.4 +click-plugins==1.1.1 click==7.0 colorama==0.4.1 dnspython==1.16.0 @@ -24,44 +24,47 @@ domaintools-api==0.3.3 enum-compat==0.0.2 ez-setup==0.9 future==0.17.1 -httplib2==0.12.1 -idna-ssl==1.1.0 ; python_version < '3.7' +httplib2==0.12.3 idna==2.8 isodate==0.6.0 jsonschema==3.0.1 maclookup==1.0.3 multidict==4.5.2 +np==1.0.2 +numpy==1.16.3 oauth2==1.9.0.post1 +opencv-python==4.1.0.25 passivetotal==1.0.30 -pillow==5.4.1 -psutil==5.6.0 +pillow==6.0.0 +psutil==5.6.1 pyeupi==1.0 pygeoip==0.3.2 -pyparsing==2.3.1 +pyparsing==2.4.0 pypdns==1.3 pypssl==2.1 pyrsistent==0.14.11 pytesseract==0.2.6 python-dateutil==2.8.0 -pyyaml==3.13 +pyyaml==5.1 +pyzbar==0.1.8 rdflib==4.2.2 -redis==3.2.0 -reportlab==3.5.13 -requests-cache==0.4.13 +redis==3.2.1 +reportlab==3.5.19 +requests-cache==0.5.0 requests==2.21.0 -shodan==1.11.1 -sigmatools==0.9 +shodan==1.12.1 +sigmatools==0.10 six==1.12.0 -soupsieve==1.8 -sparqlwrapper==1.8.2 +soupsieve==1.9.1 +sparqlwrapper==1.8.4 stix2-patterns==1.1.0 tabulate==0.8.3 -tornado==6.0.1 +tornado==6.0.2 url-normalize==1.4.1 urlarchiver==0.2 -urllib3==1.24.1 -vulners==1.4.5 -wand==0.5.1 -xlsxwriter==1.1.5 +urllib3==1.24.2 +vulners==1.5.0 +wand==0.5.3 +xlsxwriter==1.1.7 yara-python==3.8.1 yarl==1.3.0 From 7171c8ce92432e7850b8f5d6a51420c3543a36b0 Mon Sep 17 00:00:00 2001 From: Sascha Rommelfangen Date: Wed, 24 Apr 2019 13:54:21 +0200 Subject: [PATCH 087/207] initial version of OCR expansion module --- misp_modules/modules/expansion/__init__.py | 3 +- misp_modules/modules/expansion/ocr.py | 51 ++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 misp_modules/modules/expansion/ocr.py diff --git a/misp_modules/modules/expansion/__init__.py b/misp_modules/modules/expansion/__init__.py index 994b289..ec78e9b 100644 --- a/misp_modules/modules/expansion/__init__.py +++ b/misp_modules/modules/expansion/__init__.py @@ -8,4 +8,5 @@ __all__ = ['cuckoo_submit', 'vmray_submit', 'bgpranking', 'circl_passivedns', 'c 'yara_syntax_validator', 'hashdd', 'onyphe', 'onyphe_full', 'rbl', 'xforceexchange', 'sigma_syntax_validator', 'stix2_pattern_syntax_validator', 'sigma_queries', 'dbl_spamhaus', 'vulners', 'yara_query', 'macaddress_io', - 'intel471', 'backscatter_io', 'btc_scam_check', 'hibp', 'greynoise', 'macvendors', 'qrcode'] + 'intel471', 'backscatter_io', 'btc_scam_check', 'hibp', 'greynoise', 'macvendors', + 'qrcode', 'ocr'] diff --git a/misp_modules/modules/expansion/ocr.py b/misp_modules/modules/expansion/ocr.py new file mode 100644 index 0000000..afdf343 --- /dev/null +++ b/misp_modules/modules/expansion/ocr.py @@ -0,0 +1,51 @@ +import json +import re +import binascii +import cv2 +import np +import pytesseract + +misperrors = {'error': 'Error'} +mispattributes = {'input': ['attachment'], + 'output': ['freetext', 'text']} +moduleinfo = {'version': '0.1', 'author': 'Sascha Rommelfangen', + 'description': 'OCR decoder', + 'module-type': ['expansion']} + +moduleconfig = [] + + +def handler(q=False): + if q is False: + return False + q = json.loads(q) + filename = q['attachment'] + try: + img_array = np.frombuffer(binascii.a2b_base64(q['data']), np.uint8) + except Exception as e: + print(e) + err = "Couldn't fetch attachment (JSON 'data' is empty). Are you using the 'Query enrichment' action?" + misperrors['error'] = err + print(err) + return misperrors + + image = img_array + image = cv2.imdecode(img_array, cv2.IMREAD_COLOR) + try: + decoded = pytesseract.image_to_string(image) + return {'results': [{'types': ['freetext'], 'values': decoded, 'comment': "OCR from file " + filename}, + {'types': ['text'], 'values': decoded, 'comment': "ORC from file " + filename}]} + except Exception as e: + print(e) + err = "Couldn't analyze file type. Only images are supported right now." + misperrors['error'] = err + return misperrors + + +def introspection(): + return mispattributes + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo From 614fc1354b49bf281b746d3c6ca0bbbffcc21bff Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Wed, 24 Apr 2019 14:01:08 +0200 Subject: [PATCH 088/207] chg: [ocr] re module not used - removed --- misp_modules/modules/expansion/ocr.py | 1 - 1 file changed, 1 deletion(-) diff --git a/misp_modules/modules/expansion/ocr.py b/misp_modules/modules/expansion/ocr.py index afdf343..cd6baca 100644 --- a/misp_modules/modules/expansion/ocr.py +++ b/misp_modules/modules/expansion/ocr.py @@ -1,5 +1,4 @@ import json -import re import binascii import cv2 import np From 81b0082ae5cb2a42303c4629dd53af954713cb25 Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Wed, 24 Apr 2019 14:01:48 +0200 Subject: [PATCH 089/207] chg: [init] removed trailing whitespace --- misp_modules/modules/expansion/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/__init__.py b/misp_modules/modules/expansion/__init__.py index ec78e9b..03a7899 100644 --- a/misp_modules/modules/expansion/__init__.py +++ b/misp_modules/modules/expansion/__init__.py @@ -8,5 +8,5 @@ __all__ = ['cuckoo_submit', 'vmray_submit', 'bgpranking', 'circl_passivedns', 'c 'yara_syntax_validator', 'hashdd', 'onyphe', 'onyphe_full', 'rbl', 'xforceexchange', 'sigma_syntax_validator', 'stix2_pattern_syntax_validator', 'sigma_queries', 'dbl_spamhaus', 'vulners', 'yara_query', 'macaddress_io', - 'intel471', 'backscatter_io', 'btc_scam_check', 'hibp', 'greynoise', 'macvendors', + 'intel471', 'backscatter_io', 'btc_scam_check', 'hibp', 'greynoise', 'macvendors', 'qrcode', 'ocr'] From 5104bce4519d994aa868f9b576b803e393ca1ce7 Mon Sep 17 00:00:00 2001 From: Sascha Rommelfangen Date: Wed, 24 Apr 2019 14:53:03 +0200 Subject: [PATCH 090/207] renamed module --- misp_modules/modules/expansion/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/__init__.py b/misp_modules/modules/expansion/__init__.py index ec78e9b..38d59ad 100644 --- a/misp_modules/modules/expansion/__init__.py +++ b/misp_modules/modules/expansion/__init__.py @@ -9,4 +9,4 @@ __all__ = ['cuckoo_submit', 'vmray_submit', 'bgpranking', 'circl_passivedns', 'c 'xforceexchange', 'sigma_syntax_validator', 'stix2_pattern_syntax_validator', 'sigma_queries', 'dbl_spamhaus', 'vulners', 'yara_query', 'macaddress_io', 'intel471', 'backscatter_io', 'btc_scam_check', 'hibp', 'greynoise', 'macvendors', - 'qrcode', 'ocr'] + 'qrcode', 'ocr-enrich'] From 07f759b07a5994e2fea4410a4a230e2304159009 Mon Sep 17 00:00:00 2001 From: Sascha Rommelfangen Date: Wed, 24 Apr 2019 14:53:16 +0200 Subject: [PATCH 091/207] renamed file --- misp_modules/modules/expansion/{ocr.py => ocr-enrich.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename misp_modules/modules/expansion/{ocr.py => ocr-enrich.py} (100%) diff --git a/misp_modules/modules/expansion/ocr.py b/misp_modules/modules/expansion/ocr-enrich.py similarity index 100% rename from misp_modules/modules/expansion/ocr.py rename to misp_modules/modules/expansion/ocr-enrich.py From 29e57dfcc6afa59b3d68d5ccb3c65ee649001a09 Mon Sep 17 00:00:00 2001 From: Steve Clement Date: Thu, 25 Apr 2019 17:36:32 +0900 Subject: [PATCH 092/207] chg: [doc] Added new dependencies and updated RHEL/CentOS howto. --- README.md | 39 ++++++++++++++------------------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 169151e..64d0960 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ ## How to install and start MISP modules in a Python virtualenv? (recommended) ~~~~bash -sudo apt-get install python3-dev python3-pip libpq5 libjpeg-dev tesseract-ocr imagemagick virtualenv +sudo apt-get install python3-dev python3-pip libpq5 libjpeg-dev tesseract-ocr imagemagick virtualenv libopencv-dev zbar-tools sudo -u www-data virtualenv -p python3 /var/www/MISP/venv cd /usr/local/src/ sudo git clone https://github.com/MISP/misp-modules.git @@ -107,36 +107,24 @@ sudo systemctl enable --now misp-modules /var/www/MISP/venv/bin/misp-modules -l 127.0.0.1 -s & #to start the modules ~~~~ -## How to install and start MISP modules on Debian-based distributions ? - -~~~~bash -sudo apt-get install python3-dev python3-pip libpq5 libjpeg-dev tesseract-ocr imagemagick -cd /usr/local/src/ -sudo git clone https://github.com/MISP/misp-modules.git -cd misp-modules -sudo pip3 install -I -r REQUIREMENTS -sudo pip3 install -I . -# Start misp-modules as a service -sudo cp etc/systemd/system/misp-modules.service /etc/systemd/system/ -sudo systemctl daemon-reload -sudo systemctl enable --now misp-modules -/var/www/MISP/venv/bin/misp-modules -l 127.0.0.1 -s & #to start the modules -~~~~ - ## How to install and start MISP modules on RHEL-based distributions ? As of this writing, the official RHEL repositories only contain Ruby 2.0.0 and Ruby 2.1 or higher is required. As such, this guide installs Ruby 2.2 from the [SCL](https://access.redhat.com/documentation/en-us/red_hat_software_collections/3/html/3.2_release_notes/chap-installation#sect-Installation-Subscribe) repository. + ~~~~bash -yum install rh-ruby22 +sudo yum install rh-ruby22 +sudo yum install openjpeg-devel +sudo yum install rubygem-rouge rubygem-asciidoctor zbar-devel opencv-devel cd /var/www/MISP git clone https://github.com/MISP/misp-modules.git cd misp-modules -scl enable rh-python36 ‘python3 –m pip install cryptography’ -scl enable rh-python36 ‘python3 –m pip install -I -r REQUIREMENTS’ -scl enable rh-python36 ‘python3 –m pip install –I .’ +sudo -u apache /usr/bin/scl enable rh-python36 "virtualenv -p python3 /var/www/MISP/venv" +sudo -u apache /var/www/MISP/venv/bin/pip install -U -I -r REQUIREMENTS +sudo -u apache /var/www/MISP/venv/bin/pip install -U . ~~~~ + Create the service file /etc/systemd/system/misp-modules.service : ~~~~ -[Unit] +echo "[Unit] Description=MISP's modules After=misp-workers.service @@ -144,15 +132,16 @@ After=misp-workers.service Type=simple User=apache Group=apache -ExecStart=/usr/bin/scl enable rh-python36 rh-ruby22 ‘/opt/rh/rh-python36/root/bin/misp-modules –l 127.0.0.1 –s’ +ExecStart=/usr/bin/scl enable rh-python36 rh-ruby22 '/opt/rh/rh-python36/root/bin/misp-modules –l 127.0.0.1 –s' Restart=always RestartSec=10 [Install] -WantedBy=multi-user.target +WantedBy=multi-user.target" | sudo tee /etc/systemd/system/misp-modules.service ~~~~ + The `After=misp-workers.service` must be changed or removed if you have not created a misp-workers service. -Then, enable the misp-modules service and start it ; +Then, enable the misp-modules service and start it: ~~~~bash systemctl daemon-reload systemctl enable --now misp-modules From 2c64e5ca67ee37e9d18131a8ef1e8b4c50f360b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Fri, 26 Apr 2019 11:35:03 +0200 Subject: [PATCH 093/207] fix: CTRL+C is working again Fix #292 --- misp_modules/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/misp_modules/__init__.py b/misp_modules/__init__.py index d933dc9..68a2c69 100644 --- a/misp_modules/__init__.py +++ b/misp_modules/__init__.py @@ -55,7 +55,7 @@ log = logging.getLogger('misp-modules') def handle_signal(sig, frame): - IOLoop.instance().add_callback(IOLoop.instance().stop) + IOLoop.instance().add_callback_from_signal(IOLoop.instance().stop) def init_logger(level=False): @@ -266,8 +266,11 @@ def main(): if args.t: log.info('MISP modules started in test-mode, quitting immediately.') sys.exit() - IOLoop.instance().start() - IOLoop.instance().stop() + try: + IOLoop.instance().start() + finally: + IOLoop.instance().stop() + return 0 From c825cabbbe104fe3b3151f98047c1942a1825079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Fri, 26 Apr 2019 11:40:25 +0200 Subject: [PATCH 094/207] chg: Bump dependencies --- Pipfile.lock | 91 ++++++++++++++++++++++++---------------------------- REQUIREMENTS | 8 ++--- 2 files changed, 46 insertions(+), 53 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 8a073a9..5d395d5 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -170,13 +170,6 @@ ], "version": "==2.8" }, - "idna-ssl": { - "hashes": [ - "sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c" - ], - "markers": "python_version < '3.7'", - "version": "==1.1.0" - }, "isodate": { "hashes": [ "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8", @@ -354,17 +347,17 @@ }, "psutil": { "hashes": [ - "sha256:23e9cd90db94fbced5151eaaf9033ae9667c033dffe9e709da761c20138d25b6", - "sha256:27858d688a58cbfdd4434e1c40f6c79eb5014b709e725c180488ccdf2f721729", - "sha256:354601a1d1a1322ae5920ba397c58d06c29728a15113598d1a8158647aaa5385", - "sha256:9c3a768486194b4592c7ae9374faa55b37b9877fd9746fb4028cb0ac38fd4c60", - "sha256:c1fd45931889dc1812ba61a517630d126f6185f688eac1693171c6524901b7de", - "sha256:d463a142298112426ebd57351b45c39adb41341b91f033aa903fa4c6f76abecc", - "sha256:e1494d20ffe7891d07d8cb9a8b306c1a38d48b13575265d090fc08910c56d474", - "sha256:ec4b4b638b84d42fc48139f9352f6c6587ee1018d55253542ee28db7480cc653", - "sha256:fa0a570e0a30b9dd618bffbece590ae15726b47f9f1eaf7518dfb35f4d7dcd21" + "sha256:206eb909aa8878101d0eca07f4b31889c748f34ed6820a12eb3168c7aa17478e", + "sha256:649f7ffc02114dced8fbd08afcd021af75f5f5b2311bc0e69e53e8f100fe296f", + "sha256:6ebf2b9c996bb8c7198b385bade468ac8068ad8b78c54a58ff288cd5f61992c7", + "sha256:753c5988edc07da00dafd6d3d279d41f98c62cd4d3a548c4d05741a023b0c2e7", + "sha256:76fb0956d6d50e68e3f22e7cc983acf4e243dc0fcc32fd693d398cb21c928802", + "sha256:828e1c3ca6756c54ac00f1427fdac8b12e21b8a068c3bb9b631a1734cada25ed", + "sha256:a4c62319ec6bf2b3570487dd72d471307ae5495ce3802c1be81b8a22e438b4bc", + "sha256:acba1df9da3983ec3c9c963adaaf530fcb4be0cd400a8294f1ecc2db56499ddd", + "sha256:ef342cb7d9b60e6100364f50c57fa3a77d02ff8665d5b956746ac01901247ac4" ], - "version": "==5.6.1" + "version": "==5.6.2" }, "pybgpranking": { "editable": true, @@ -406,7 +399,7 @@ "pymisp": { "editable": true, "git": "https://github.com/MISP/PyMISP.git", - "ref": "921f414e0e026a3a4b77112012cf930242a33b04" + "ref": "582dda0ce2a8ca8e1dd2cf3842e0491caca51c62" }, "pyonyphe": { "editable": true, @@ -436,9 +429,9 @@ }, "pyrsistent": { "hashes": [ - "sha256:3ca82748918eb65e2d89f222b702277099aca77e34843c5eb9d52451173970e2" + "sha256:5403d37f4d55ff4572b5b5676890589f367a9569529c6f728c11046c4ea4272b" ], - "version": "==0.14.11" + "version": "==0.15.1" }, "pytesseract": { "hashes": [ @@ -495,37 +488,37 @@ }, "reportlab": { "hashes": [ - "sha256:1c228a3ac2c405f7fc16eac43ba92aec448bc25438902f30590ad021e8828097", - "sha256:2210fafd3bb06308a84876fe6d19172b645373edce2b6d7501378cb9c768f825", - "sha256:232fb2037b7c3df259685f1c5ecb7826f55742dc81f0713837b84a152307483e", - "sha256:2c4f25e63fa75f3064871cf435696a4e19b7bd4901d922b766ae58a447b5b6da", - "sha256:47951166d897b60e9e7ca349db82a2b689e6478ac6078e2c7c88ca8becbb0c7d", - "sha256:526ab1193ea8e97c4838135917890e66de5f777d04283008007229b139f3c094", - "sha256:5a9cc8470623ec5b76c7e59f56b7d1fcf0254896cd61842dbdbd278934cc50f4", - "sha256:5ddc1a4a74f225e35a7f60e2eae10de6878dddc9960dad2d9cadc49092f8850d", - "sha256:6b594f6d7d71bc5778e19adb1c699a598c69b9a7bcf97fa638d8762279f9d80a", - "sha256:6e8c89b46cfaf9ae40b7db87e9f29c9e5d32d18d25f9cd10d423a5241e8ec453", - "sha256:71f4f3e3975b91ddbfc1b36a537b46d07533ca7f31945e990a75db5f9bd7a0ba", - "sha256:763654dc346eeb66fa726a88d27f911339950d20a25303dfc098f3b59ba26614", - "sha256:7bae4b33363f44343e0fac5004c8e44576c3ed00885be4eee1f2260802c116c3", - "sha256:8a4b8a0fd0547f3b436b548284aa604ba183bfac26f41a7ffb23d0ff5db8c658", - "sha256:8b08d68e4cb498eabf85411beda5c32e591ef8d0a6d18c948c3f80ed5d2c6e31", - "sha256:9840f27948b54aefa3c6386e5ed0f124d641eb54fa2f2bc9aebcb270598487fc", - "sha256:9ae8f822370e47486ba1880f7580669058a41e64bdaa41019f4617317489f884", - "sha256:9db49197080646a113059eba1c0758161164de1bc57315e7422bbf8c86e03dcf", - "sha256:a08d23fa3f23f13a1cc6dca3b3c431d08ae48e52384e6bf47bbefb22fde58e61", - "sha256:ac111bc47733dbfa3e34d61282c91b69b1f66800b0c72b7b86dc2534faa09bef", - "sha256:bc3c69707c0bf9308193612d34ca87249d6fc91a35ce0873102321395d39024a", - "sha256:c375759a763c1c93d5b4f36620390440d9fa6dec6fcf88bce8234701d88b339c", - "sha256:c8a5988d73ec93a54f22660b64c5f3d2018163dd9ca4a5cdde8022a7e4fcb345", - "sha256:eba2bc7c28a3b2b0a3c24caff33e4d8708db008f480b03a6ea39c28661663746", - "sha256:ee187977d587b9b81929e08022f385eb11274efd75795d59d99eb23b3fa9b055", - "sha256:f3ef7616ffc27c150ffec61ac820739495f6a9ca5d8532047102756ebb27e8d1", - "sha256:f46f223fcae09c8bf2746b4eb2f351294faae04b262429cc480d34c69b133fd9", - "sha256:fd9f6429a68a246fb466696d97d1240752c889b5bfdc219fea15ae787cf366a6" + "sha256:13714baa9753bfca94df67716cccb3eedcaaa30cf7bc40b282d338a718e0b610", + "sha256:16c1bb717a1a0e2ed065aa31eb5968dc03b34b728926216ef282cefeebf50c1b", + "sha256:2d9d66770880e8d112b6b925458593d34b84947c355847578cd974df0a3e3b8b", + "sha256:3334a30e477e1dfa0276eb41ed5bfd2a684c9917e55c6acb30d91abac46555f6", + "sha256:33796ea88d20c05958903c11ff34d896e462381f4a0f550854aabe6dd07cc189", + "sha256:5184f53c0babeedb4ebe297eb97794822cb122456ca03411c68256730c998d48", + "sha256:53589c5db35041920cd7a92a171506ff4eb5542ab8415af272fe4558927399a8", + "sha256:58ba0a9ca51d666d55ec7ecd83ab14763b79e7e5e0775b7717694e94c2fbbf18", + "sha256:6998652beba357da9687eba13b46ceccd0a7a2153d656cf8a03b7494c915e077", + "sha256:6c3b07c8a94ee9609c31a51e4131891c8330ffd379db23ab582fd225a06a4e59", + "sha256:7b248d2d9d4ab6d4cad91eb2b153b2c4c7b3fced89cb5a5b5bfbc7d09593871a", + "sha256:81d991c9994a576ea053b281b8c9afc28b12261197d478e72055d381f60fa26f", + "sha256:8a9a8be6841b88b13aa9c0f7d193c6d24b04b10c2e7cbf6657b1807bac5b1e9f", + "sha256:8de3107436e68014890adcec446207fd98d60c26e7feae6f550eea9eab3a622d", + "sha256:90f85afb68f7cd4fd8681af3123d23488274e4d1c3fea8d3831ef7257d9733c8", + "sha256:94857052c951ffa56de95cfce483fdf3de19587db4b1bc4f6a4043fb1c4af772", + "sha256:a47603d9b975e8870ed30ade22db3478b030dd7a6041c8272c3719d9bbeaef34", + "sha256:a5671b152d3f85963d8450e956ddecfb5d30af62dd1f73207fab9aa32a0240d2", + "sha256:a745cd1a4368fac093deff3b65614f052eced6afa9ed7fe223da2a52813f2e23", + "sha256:af454e8e844e3eeace5aead02701748b2a908f3e8cbc386cc5ddc185cef8c57f", + "sha256:c3c6c1234eed451111e969c776688e866554cb362250b88f782ab80ea62f9114", + "sha256:cc1cf8ba1b2f1669f5d873a7cfdb9e07a920243d74a66a78f8afa2cf78587864", + "sha256:cce3c9b0e115ea5553615a647b6644e5724bdc776e778593ffa5f383d907afb2", + "sha256:d137feacef83627d10adb869aa6998f29eb7af4cff3101c9fc94a1d73943b6cc", + "sha256:d7213814d050ca3b0bf7e018f94ed947e90477cd36aff298ff5932b849a0f36a", + "sha256:e381d08675807d2bb465717f69818040173351650af82730d721ecad429279a6", + "sha256:e39c6efdd64027be56ce991f7ffb86c7cee47da8c844c3544bbd68ef842831a0", + "sha256:f8526cfbbad599d22de6eb59b5a43610ba9b28f74ac2406125fe803f31a262a6" ], "index": "pypi", - "version": "==3.5.19" + "version": "==3.5.20" }, "requests": { "hashes": [ diff --git a/REQUIREMENTS b/REQUIREMENTS index b9d198a..37c5a68 100644 --- a/REQUIREMENTS +++ b/REQUIREMENTS @@ -3,7 +3,7 @@ -e git+https://github.com/D4-project/BGP-Ranking.git/@4e0741056bcc0077de1120b8724a31330b26033e#egg=pybgpranking&subdirectory=client -e git+https://github.com/D4-project/IPASN-History.git/@c0c2bbf8d70811982dad065ea463a7e01593a38d#egg=pyipasnhistory&subdirectory=client -e git+https://github.com/MISP/PyIntel471.git@0df8d51f1c1425de66714b3a5a45edb69b8cc2fc#egg=pyintel471 --e git+https://github.com/MISP/PyMISP.git@921f414e0e026a3a4b77112012cf930242a33b04#egg=pymisp +-e git+https://github.com/MISP/PyMISP.git@582dda0ce2a8ca8e1dd2cf3842e0491caca51c62#egg=pymisp -e git+https://github.com/Rafiot/uwhoisd.git@411572840eba4c72dc321c549b36a54ed5cea9de#egg=uwhois&subdirectory=client -e git+https://github.com/sebdraven/pydnstrails@48c1f740025c51289f43a24863d1845ff12fd21a#egg=pydnstrails -e git+https://github.com/sebdraven/pyonyphe@cbb0168d5cb28a9f71f7ab3773164a7039ccdb12#egg=pyonyphe @@ -36,20 +36,20 @@ oauth2==1.9.0.post1 opencv-python==4.1.0.25 passivetotal==1.0.30 pillow==6.0.0 -psutil==5.6.1 +psutil==5.6.2 pyeupi==1.0 pygeoip==0.3.2 pyparsing==2.4.0 pypdns==1.3 pypssl==2.1 -pyrsistent==0.14.11 +pyrsistent==0.15.1 pytesseract==0.2.6 python-dateutil==2.8.0 pyyaml==5.1 pyzbar==0.1.8 rdflib==4.2.2 redis==3.2.1 -reportlab==3.5.19 +reportlab==3.5.20 requests-cache==0.5.0 requests==2.21.0 shodan==1.12.1 From f55d7946df192032f6b8b8a7a0e78efd30547f14 Mon Sep 17 00:00:00 2001 From: Sascha Rommelfangen Date: Fri, 26 Apr 2019 12:07:55 +0200 Subject: [PATCH 095/207] introduction of new modules --- misp_modules/modules/expansion/docx-enrich.py | 61 +++++++++++++++++++ misp_modules/modules/expansion/ods-enrich.py | 56 +++++++++++++++++ misp_modules/modules/expansion/odt-enrich.py | 51 ++++++++++++++++ misp_modules/modules/expansion/pdf-enrich.py | 50 +++++++++++++++ misp_modules/modules/expansion/pptx-enrich.py | 55 +++++++++++++++++ misp_modules/modules/expansion/xlsx-enrich.py | 53 ++++++++++++++++ 6 files changed, 326 insertions(+) create mode 100644 misp_modules/modules/expansion/docx-enrich.py create mode 100644 misp_modules/modules/expansion/ods-enrich.py create mode 100644 misp_modules/modules/expansion/odt-enrich.py create mode 100644 misp_modules/modules/expansion/pdf-enrich.py create mode 100644 misp_modules/modules/expansion/pptx-enrich.py create mode 100644 misp_modules/modules/expansion/xlsx-enrich.py diff --git a/misp_modules/modules/expansion/docx-enrich.py b/misp_modules/modules/expansion/docx-enrich.py new file mode 100644 index 0000000..6380557 --- /dev/null +++ b/misp_modules/modules/expansion/docx-enrich.py @@ -0,0 +1,61 @@ +import json +import binascii +import np +import docx +import io + +misperrors = {'error': 'Error'} +mispattributes = {'input': ['attachment'], + 'output': ['freetext', 'text']} +moduleinfo = {'version': '0.1', 'author': 'Sascha Rommelfangen', + 'description': '.docx to freetext-import IOC extractor', + 'module-type': ['expansion']} + +moduleconfig = [] + + +def handler(q=False): + if q is False: + return False + q = json.loads(q) + filename = q['attachment'] + try: + docx_array = np.frombuffer(binascii.a2b_base64(q['data']), np.uint8) + except Exception as e: + print(e) + err = "Couldn't fetch attachment (JSON 'data' is empty). Are you using the 'Query enrichment' action?" + misperrors['error'] = err + print(err) + return misperrors + + doc_content = "" + doc_file = io.BytesIO(docx_array) + try: + doc = docx.Document(doc_file) + for para in doc.paragraphs: + print(para.text) + doc_content = doc_content + "\n" + para.text + tables = doc.tables + for table in tables: + for row in table.rows: + for cell in row.cells: + for para in cell.paragraphs: + print(para.text) + doc_content = doc_content + "\n" + para.text + print(doc_content) + return {'results': [{'types': ['freetext'], 'values': doc_content, 'comment': ".docx-to-text from file " + filename}, + {'types': ['text'], 'values': doc_content, 'comment': ".docx-to-text from file " + filename}]} + except Exception as e: + print(e) + err = "Couldn't analyze file as .docx. Error was: " + str(e) + misperrors['error'] = err + return misperrors + + +def introspection(): + return mispattributes + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo diff --git a/misp_modules/modules/expansion/ods-enrich.py b/misp_modules/modules/expansion/ods-enrich.py new file mode 100644 index 0000000..06d7ff5 --- /dev/null +++ b/misp_modules/modules/expansion/ods-enrich.py @@ -0,0 +1,56 @@ +import json +import binascii +import np +import ezodf +import pandas_ods_reader +import io + +misperrors = {'error': 'Error'} +mispattributes = {'input': ['attachment'], + 'output': ['freetext', 'text']} +moduleinfo = {'version': '0.1', 'author': 'Sascha Rommelfangen', + 'description': '.ods to freetext-import IOC extractor', + 'module-type': ['expansion']} + +moduleconfig = [] + + +def handler(q=False): + if q is False: + return False + q = json.loads(q) + filename = q['attachment'] + try: + ods_array = np.frombuffer(binascii.a2b_base64(q['data']), np.uint8) + except Exception as e: + print(e) + err = "Couldn't fetch attachment (JSON 'data' is empty). Are you using the 'Query enrichment' action?" + misperrors['error'] = err + print(err) + return misperrors + + ods_content = "" + ods_file = io.BytesIO(ods_array) + doc = ezodf.opendoc(ods_file) + num_sheets = len(doc.sheets) + try: + for i in range(0, num_sheets): + ods = pandas_ods_reader.read_ods(ods_file, i, headers=False) + ods_content = ods_content + "\n" + ods.to_string(max_rows=None) + print(ods_content) + return {'results': [{'types': ['freetext'], 'values': ods_content, 'comment': ".ods-to-text from file " + filename}, + {'types': ['text'], 'values': ods_content, 'comment': ".ods-to-text from file " + filename}]} + except Exception as e: + print(e) + err = "Couldn't analyze file as .ods. Error was: " + str(e) + misperrors['error'] = err + return misperrors + + +def introspection(): + return mispattributes + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo diff --git a/misp_modules/modules/expansion/odt-enrich.py b/misp_modules/modules/expansion/odt-enrich.py new file mode 100644 index 0000000..6dac555 --- /dev/null +++ b/misp_modules/modules/expansion/odt-enrich.py @@ -0,0 +1,51 @@ +import json +import binascii +import np +from ODTReader.odtreader import odtToText +import io + +misperrors = {'error': 'Error'} +mispattributes = {'input': ['attachment'], + 'output': ['freetext', 'text']} +moduleinfo = {'version': '0.1', 'author': 'Sascha Rommelfangen', + 'description': '.odt to freetext-import IOC extractor', + 'module-type': ['expansion']} + +moduleconfig = [] + + +def handler(q=False): + if q is False: + return False + q = json.loads(q) + filename = q['attachment'] + try: + odt_array = np.frombuffer(binascii.a2b_base64(q['data']), np.uint8) + except Exception as e: + print(e) + err = "Couldn't fetch attachment (JSON 'data' is empty). Are you using the 'Query enrichment' action?" + misperrors['error'] = err + print(err) + return misperrors + + odt_content = "" + odt_file = io.BytesIO(odt_array) + try: + odt_content = odtToText(odt_file) + print(odt_content) + return {'results': [{'types': ['freetext'], 'values': odt_content, 'comment': ".odt-to-text from file " + filename}, + {'types': ['text'], 'values': odt_content, 'comment': ".odt-to-text from file " + filename}]} + except Exception as e: + print(e) + err = "Couldn't analyze file as .odt. Error was: " + str(e) + misperrors['error'] = err + return misperrors + + +def introspection(): + return mispattributes + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo diff --git a/misp_modules/modules/expansion/pdf-enrich.py b/misp_modules/modules/expansion/pdf-enrich.py new file mode 100644 index 0000000..aeb5d9b --- /dev/null +++ b/misp_modules/modules/expansion/pdf-enrich.py @@ -0,0 +1,50 @@ +import json +import binascii +import np +import pytesseract +import pdftotext +import io +import collections + +misperrors = {'error': 'Error'} +mispattributes = {'input': ['attachment'], + 'output': ['freetext', 'text']} +moduleinfo = {'version': '0.1', 'author': 'Sascha Rommelfangen', + 'description': 'PDF to freetext-import IOC extractor', + 'module-type': ['expansion']} + +moduleconfig = [] + + +def handler(q=False): + if q is False: + return False + q = json.loads(q) + filename = q['attachment'] + try: + pdf_array = np.frombuffer(binascii.a2b_base64(q['data']), np.uint8) + except Exception as e: + print(e) + err = "Couldn't fetch attachment (JSON 'data' is empty). Are you using the 'Query enrichment' action?" + misperrors['error'] = err + print(err) + return misperrors + + pdf_file = io.BytesIO(pdf_array) + try: + pdf_content = "\n\n".join(pdftotext.PDF(pdf_file)) + return {'results': [{'types': ['freetext'], 'values': pdf_content, 'comment': "PDF-to-text from file " + filename}]} + except Exception as e: + print(e) + err = "Couldn't analyze file as PDF. Error was: " + str(e) + misperrors['error'] = err + return misperrors + + +def introspection(): + return mispattributes + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo diff --git a/misp_modules/modules/expansion/pptx-enrich.py b/misp_modules/modules/expansion/pptx-enrich.py new file mode 100644 index 0000000..9381262 --- /dev/null +++ b/misp_modules/modules/expansion/pptx-enrich.py @@ -0,0 +1,55 @@ +import json +import binascii +import np +from pptx import Presentation +import io + +misperrors = {'error': 'Error'} +mispattributes = {'input': ['attachment'], + 'output': ['freetext', 'text']} +moduleinfo = {'version': '0.1', 'author': 'Sascha Rommelfangen', + 'description': '.pptx to freetext-import IOC extractor', + 'module-type': ['expansion']} + +moduleconfig = [] + + +def handler(q=False): + if q is False: + return False + q = json.loads(q) + filename = q['attachment'] + try: + pptx_array = np.frombuffer(binascii.a2b_base64(q['data']), np.uint8) + except Exception as e: + print(e) + err = "Couldn't fetch attachment (JSON 'data' is empty). Are you using the 'Query enrichment' action?" + misperrors['error'] = err + print(err) + return misperrors + + ppt_content = "" + ppt_file = io.BytesIO(pptx_array) + try: + ppt = Presentation(ppt_file) + for slide in ppt.slides: + for shape in slide.shapes: + if hasattr(shape, "text"): + print(shape.text) + ppt_content = ppt_content + "\n" + shape.text + return {'results': [{'types': ['freetext'], 'values': ppt_content, 'comment': ".pptx-to-text from file " + filename}, + {'types': ['text'], 'values': ppt_content, 'comment': ".pptx-to-text from file " + filename}]} + except Exception as e: + print(e) + err = "Couldn't analyze file as .pptx. Error was: " + str(e) + misperrors['error'] = err + return misperrors + + +def introspection(): + return mispattributes + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo diff --git a/misp_modules/modules/expansion/xlsx-enrich.py b/misp_modules/modules/expansion/xlsx-enrich.py new file mode 100644 index 0000000..66df3e1 --- /dev/null +++ b/misp_modules/modules/expansion/xlsx-enrich.py @@ -0,0 +1,53 @@ +import json +import binascii +import np +import pandas +import io + +misperrors = {'error': 'Error'} +mispattributes = {'input': ['attachment'], + 'output': ['freetext', 'text']} +moduleinfo = {'version': '0.1', 'author': 'Sascha Rommelfangen', + 'description': '.xlsx to freetext-import IOC extractor', + 'module-type': ['expansion']} + +moduleconfig = [] + + +def handler(q=False): + if q is False: + return False + q = json.loads(q) + filename = q['attachment'] + try: + xlsx_array = np.frombuffer(binascii.a2b_base64(q['data']), np.uint8) + except Exception as e: + print(e) + err = "Couldn't fetch attachment (JSON 'data' is empty). Are you using the 'Query enrichment' action?" + misperrors['error'] = err + print(err) + return misperrors + + xls_content = "" + xls_file = io.BytesIO(xlsx_array) + pandas.set_option('display.max_colwidth', -1) + try: + xls = pandas.read_excel(xls_file) + xls_content = xls.to_string(max_rows=None) + print(xls_content) + return {'results': [{'types': ['freetext'], 'values': xls_content, 'comment': ".xlsx-to-text from file " + filename}, + {'types': ['text'], 'values': xls_content, 'comment': ".xlsx-to-text from file " + filename}]} + except Exception as e: + print(e) + err = "Couldn't analyze file as .xlsx. Error was: " + str(e) + misperrors['error'] = err + return misperrors + + +def introspection(): + return mispattributes + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo From 3c0319b8bc80a60e2e33d0aebd56a2036d98d791 Mon Sep 17 00:00:00 2001 From: Sascha Rommelfangen Date: Fri, 26 Apr 2019 12:08:52 +0200 Subject: [PATCH 096/207] new requirements for new modules --- .travis.yml | 2 +- Pipfile | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 52a7742..18c02c6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ python: - "3.7-dev" install: - - sudo apt-get install libzbar0 libzbar-dev + - sudo apt-get install libzbar0 libzbar-dev libpoppler-cpp-dev - pip install pipenv - pipenv install --dev diff --git a/Pipfile b/Pipfile index ba650fe..8529abc 100644 --- a/Pipfile +++ b/Pipfile @@ -45,6 +45,16 @@ backscatter = "*" pyzbar = "*" opencv-python = "*" np = "*" +ODTReader = {editable = true,git = "https://github.com/cartertemm/ODTReader.git"} +python-pptx = "*" +collections = "*" +python-docx = "*" +ezodf = "*" +pandas = "*" +pandas_ods_reader = "*" +pdftotext = "*" +lxml = "*" +xlrd = "*" [requires] python_version = "3" From 1d4f8a6989b2741ae0f58d11f960b0f8aa0dafcf Mon Sep 17 00:00:00 2001 From: Sascha Rommelfangen Date: Fri, 26 Apr 2019 12:09:16 +0200 Subject: [PATCH 097/207] new modules added --- misp_modules/modules/expansion/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/__init__.py b/misp_modules/modules/expansion/__init__.py index 5c3edee..c4b2fdc 100644 --- a/misp_modules/modules/expansion/__init__.py +++ b/misp_modules/modules/expansion/__init__.py @@ -9,4 +9,5 @@ __all__ = ['cuckoo_submit', 'vmray_submit', 'bgpranking', 'circl_passivedns', 'c 'xforceexchange', 'sigma_syntax_validator', 'stix2_pattern_syntax_validator', 'sigma_queries', 'dbl_spamhaus', 'vulners', 'yara_query', 'macaddress_io', 'intel471', 'backscatter_io', 'btc_scam_check', 'hibp', 'greynoise', 'macvendors', - 'qrcode', 'ocr-enrich'] + 'qrcode', 'ocr-enrich', 'pdf-enrich', 'docx-enrich', 'xlsx-enrich', 'pptx-enrich', + 'ods-enrich', 'odt-enrich'] From fc339c888db7cb844ce8233129c97c1e41ec7664 Mon Sep 17 00:00:00 2001 From: Sascha Rommelfangen Date: Fri, 26 Apr 2019 12:14:56 +0200 Subject: [PATCH 098/207] removed trailing whitespaces --- misp_modules/modules/expansion/docx-enrich.py | 2 +- misp_modules/modules/expansion/ods-enrich.py | 4 ++-- misp_modules/modules/expansion/odt-enrich.py | 2 +- misp_modules/modules/expansion/pdf-enrich.py | 2 +- misp_modules/modules/expansion/pptx-enrich.py | 2 +- misp_modules/modules/expansion/xlsx-enrich.py | 6 +++--- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/misp_modules/modules/expansion/docx-enrich.py b/misp_modules/modules/expansion/docx-enrich.py index 6380557..d5da3f8 100644 --- a/misp_modules/modules/expansion/docx-enrich.py +++ b/misp_modules/modules/expansion/docx-enrich.py @@ -28,7 +28,7 @@ def handler(q=False): print(err) return misperrors - doc_content = "" + doc_content = "" doc_file = io.BytesIO(docx_array) try: doc = docx.Document(doc_file) diff --git a/misp_modules/modules/expansion/ods-enrich.py b/misp_modules/modules/expansion/ods-enrich.py index 06d7ff5..b247c44 100644 --- a/misp_modules/modules/expansion/ods-enrich.py +++ b/misp_modules/modules/expansion/ods-enrich.py @@ -29,14 +29,14 @@ def handler(q=False): print(err) return misperrors - ods_content = "" + ods_content = "" ods_file = io.BytesIO(ods_array) doc = ezodf.opendoc(ods_file) num_sheets = len(doc.sheets) try: for i in range(0, num_sheets): ods = pandas_ods_reader.read_ods(ods_file, i, headers=False) - ods_content = ods_content + "\n" + ods.to_string(max_rows=None) + ods_content = ods_content + "\n" + ods.to_string(max_rows=None) print(ods_content) return {'results': [{'types': ['freetext'], 'values': ods_content, 'comment': ".ods-to-text from file " + filename}, {'types': ['text'], 'values': ods_content, 'comment': ".ods-to-text from file " + filename}]} diff --git a/misp_modules/modules/expansion/odt-enrich.py b/misp_modules/modules/expansion/odt-enrich.py index 6dac555..c4513ae 100644 --- a/misp_modules/modules/expansion/odt-enrich.py +++ b/misp_modules/modules/expansion/odt-enrich.py @@ -28,7 +28,7 @@ def handler(q=False): print(err) return misperrors - odt_content = "" + odt_content = "" odt_file = io.BytesIO(odt_array) try: odt_content = odtToText(odt_file) diff --git a/misp_modules/modules/expansion/pdf-enrich.py b/misp_modules/modules/expansion/pdf-enrich.py index aeb5d9b..59d003e 100644 --- a/misp_modules/modules/expansion/pdf-enrich.py +++ b/misp_modules/modules/expansion/pdf-enrich.py @@ -2,7 +2,7 @@ import json import binascii import np import pytesseract -import pdftotext +import pdftotext import io import collections diff --git a/misp_modules/modules/expansion/pptx-enrich.py b/misp_modules/modules/expansion/pptx-enrich.py index 9381262..816e439 100644 --- a/misp_modules/modules/expansion/pptx-enrich.py +++ b/misp_modules/modules/expansion/pptx-enrich.py @@ -28,7 +28,7 @@ def handler(q=False): print(err) return misperrors - ppt_content = "" + ppt_content = "" ppt_file = io.BytesIO(pptx_array) try: ppt = Presentation(ppt_file) diff --git a/misp_modules/modules/expansion/xlsx-enrich.py b/misp_modules/modules/expansion/xlsx-enrich.py index 66df3e1..6e0ee73 100644 --- a/misp_modules/modules/expansion/xlsx-enrich.py +++ b/misp_modules/modules/expansion/xlsx-enrich.py @@ -1,7 +1,7 @@ import json import binascii import np -import pandas +import pandas import io misperrors = {'error': 'Error'} @@ -28,12 +28,12 @@ def handler(q=False): print(err) return misperrors - xls_content = "" + xls_content = "" xls_file = io.BytesIO(xlsx_array) pandas.set_option('display.max_colwidth', -1) try: xls = pandas.read_excel(xls_file) - xls_content = xls.to_string(max_rows=None) + xls_content = xls.to_string(max_rows=None) print(xls_content) return {'results': [{'types': ['freetext'], 'values': xls_content, 'comment': ".xlsx-to-text from file " + filename}, {'types': ['text'], 'values': xls_content, 'comment': ".xlsx-to-text from file " + filename}]} From 73067c8b238b563a11721281a3d58c96a678694b Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Fri, 26 Apr 2019 13:28:16 +0200 Subject: [PATCH 099/207] chg: [Pipfile] collection removed --- Pipfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Pipfile b/Pipfile index 8529abc..16ac02e 100644 --- a/Pipfile +++ b/Pipfile @@ -47,7 +47,6 @@ opencv-python = "*" np = "*" ODTReader = {editable = true,git = "https://github.com/cartertemm/ODTReader.git"} python-pptx = "*" -collections = "*" python-docx = "*" ezodf = "*" pandas = "*" From 63c12f34e6eb5b3e1998eec4dd762a1dc7a82beb Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Fri, 26 Apr 2019 13:36:07 +0200 Subject: [PATCH 100/207] chg: [pdf-enrich] updated --- misp_modules/modules/expansion/pdf-enrich.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/misp_modules/modules/expansion/pdf-enrich.py b/misp_modules/modules/expansion/pdf-enrich.py index 59d003e..ef85fde 100644 --- a/misp_modules/modules/expansion/pdf-enrich.py +++ b/misp_modules/modules/expansion/pdf-enrich.py @@ -1,10 +1,8 @@ import json import binascii import np -import pytesseract import pdftotext import io -import collections misperrors = {'error': 'Error'} mispattributes = {'input': ['attachment'], From ec766f571cf00429b43becec9d3a618c603281b7 Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Fri, 26 Apr 2019 13:36:53 +0200 Subject: [PATCH 101/207] chg: [init] cleanup for pep --- misp_modules/modules/expansion/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/__init__.py b/misp_modules/modules/expansion/__init__.py index c4b2fdc..70aca68 100644 --- a/misp_modules/modules/expansion/__init__.py +++ b/misp_modules/modules/expansion/__init__.py @@ -9,5 +9,5 @@ __all__ = ['cuckoo_submit', 'vmray_submit', 'bgpranking', 'circl_passivedns', 'c 'xforceexchange', 'sigma_syntax_validator', 'stix2_pattern_syntax_validator', 'sigma_queries', 'dbl_spamhaus', 'vulners', 'yara_query', 'macaddress_io', 'intel471', 'backscatter_io', 'btc_scam_check', 'hibp', 'greynoise', 'macvendors', - 'qrcode', 'ocr-enrich', 'pdf-enrich', 'docx-enrich', 'xlsx-enrich', 'pptx-enrich', + 'qrcode', 'ocr-enrich', 'pdf-enrich', 'docx-enrich', 'xlsx-enrich', 'pptx-enrich', 'ods-enrich', 'odt-enrich'] From 48c158271b5e3b0285ec8a4b915d1cf9d16784b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Fri, 26 Apr 2019 13:48:38 +0200 Subject: [PATCH 102/207] new: Devel mode. Fix #293 --- misp_modules/__init__.py | 52 ++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/misp_modules/__init__.py b/misp_modules/__init__.py index 68a2c69..440ad3f 100644 --- a/misp_modules/__init__.py +++ b/misp_modules/__init__.py @@ -210,37 +210,59 @@ class QueryModule(tornado.web.RequestHandler): self.finish() +def _launch_from_current_dir(): + log.info('Launch MISP modules server from current directory.') + os.chdir(os.path.dirname(__file__)) + modulesdir = 'modules' + helpersdir = 'helpers' + load_helpers(helpersdir=helpersdir) + return load_modules(modulesdir) + + def main(): global mhandlers global loaded_modules signal.signal(signal.SIGINT, handle_signal) signal.signal(signal.SIGTERM, handle_signal) - argParser = argparse.ArgumentParser(description='misp-modules server') + argParser = argparse.ArgumentParser(description='misp-modules server', formatter_class=argparse.RawTextHelpFormatter) argParser.add_argument('-t', default=False, action='store_true', help='Test mode') argParser.add_argument('-s', default=False, action='store_true', help='Run a system install (package installed via pip)') argParser.add_argument('-d', default=False, action='store_true', help='Enable debugging') argParser.add_argument('-p', default=6666, help='misp-modules TCP port (default 6666)') argParser.add_argument('-l', default='localhost', help='misp-modules listen address (default localhost)') argParser.add_argument('-m', default=[], action='append', help='Register a custom module') + argParser.add_argument('--devel', default=False, action='store_true', help='''Start in development mode, enable debug, start only the module(s) listed in -m.\nExample: -m misp_modules.modules.expansion.bgpranking''') args = argParser.parse_args() port = args.p listen = args.l - log = init_logger(level=args.d) - if args.s: - log.info('Launch MISP modules server from package.') - load_package_helpers() - mhandlers, loaded_modules = load_package_modules() + if args.devel: + log = init_logger(level=True) + log.info('Launch MISP modules server in developement mode. Enable debug, load a list of modules is -m is used.') + if args.m: + mhandlers = {} + modules = [] + for module in args.m: + splitted = module.split(".") + modulename = splitted[-1] + moduletype = splitted[2] + mhandlers[modulename] = importlib.import_module(module) + mhandlers['type:' + modulename] = moduletype + modules.append(modulename) + log.info('MISP modules {0} imported'.format(modulename)) + else: + mhandlers, loaded_modules = _launch_from_current_dir() else: - log.info('Launch MISP modules server from current directory.') - os.chdir(os.path.dirname(__file__)) - modulesdir = 'modules' - helpersdir = 'helpers' - load_helpers(helpersdir=helpersdir) - mhandlers, loaded_modules = load_modules(modulesdir) + log = init_logger(level=args.d) + if args.s: + log.info('Launch MISP modules server from package.') + load_package_helpers() + mhandlers, loaded_modules = load_package_modules() + else: + mhandlers, loaded_modules = _launch_from_current_dir() - for module in args.m: - mispmod = importlib.import_module(module) - mispmod.register(mhandlers, loaded_modules) + for module in args.m: + mispmod = importlib.import_module(module) + mispmod.register(mhandlers, loaded_modules) service = [(r'/modules', ListModules), (r'/query', QueryModule)] From 929dbd2463f159f23de7df4b0089409af5670b8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Fri, 26 Apr 2019 13:49:16 +0200 Subject: [PATCH 103/207] chg: Bump dependencies. --- Pipfile | 2 +- Pipfile.lock | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 116 insertions(+), 2 deletions(-) diff --git a/Pipfile b/Pipfile index 16ac02e..762e4c5 100644 --- a/Pipfile +++ b/Pipfile @@ -45,7 +45,7 @@ backscatter = "*" pyzbar = "*" opencv-python = "*" np = "*" -ODTReader = {editable = true,git = "https://github.com/cartertemm/ODTReader.git"} +ODTReader = {editable = true,git = "https://github.com/cartertemm/ODTReader.git/"} python-pptx = "*" python-docx = "*" ezodf = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 5d395d5..6ac6493 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "7fee9399d8a7151a79b6f8bbce64564062fd562b0a091fd45a875884d3fb954e" + "sha256": "1d4c69ced012268e1ab20cee76f76652e1acd9f5133636551d5686f15748d3b4" }, "pipfile-spec": 6, "requires": { @@ -150,6 +150,13 @@ ], "version": "==0.9" }, + "ezodf": { + "hashes": [ + "sha256:000da534f689c6d55297a08f9e2ed7eada9810d194d31d164388162fb391122d" + ], + "index": "pypi", + "version": "==0.3.2" + }, "future": { "hashes": [ "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8" @@ -184,6 +191,38 @@ ], "version": "==3.0.1" }, + "lxml": { + "hashes": [ + "sha256:03984196d00670b2ab14ae0ea83d5cc0cfa4f5a42558afa9ab5fa745995328f5", + "sha256:0815b0c9f897468de6a386dc15917a0becf48cc92425613aa8bbfc7f0f82951f", + "sha256:175f3825f075cf02d15099eb52658457cf0ff103dcf11512b5d2583e1d40f58b", + "sha256:30e14c62d88d1e01a26936ecd1c6e784d4afc9aa002bba4321c5897937112616", + "sha256:3210da6f36cf4b835ff1be853962b22cc354d506f493b67a4303c88bbb40d57b", + "sha256:40f60819fbd5bad6e191ba1329bfafa09ab7f3f174b3d034d413ef5266963294", + "sha256:43b26a865a61549919f8a42e094dfdb62847113cf776d84bd6b60e4e3fc20ea3", + "sha256:4a03dd682f8e35a10234904e0b9508d705ff98cf962c5851ed052e9340df3d90", + "sha256:62f382cddf3d2e52cf266e161aa522d54fd624b8cc567bc18f573d9d50d40e8e", + "sha256:7b98f0325be8450da70aa4a796c4f06852949fe031878b4aa1d6c417a412f314", + "sha256:846a0739e595871041385d86d12af4b6999f921359b38affb99cdd6b54219a8f", + "sha256:a3080470559938a09a5d0ec558c005282e99ac77bf8211fb7b9a5c66390acd8d", + "sha256:ad841b78a476623955da270ab8d207c3c694aa5eba71f4792f65926dc46c6ee8", + "sha256:afdd75d9735e44c639ffd6258ce04a2de3b208f148072c02478162d0944d9da3", + "sha256:b4fbf9b552faff54742bcd0791ab1da5863363fb19047e68f6592be1ac2dab33", + "sha256:b90c4e32d6ec089d3fa3518436bdf5ce4d902a0787dbd9bb09f37afe8b994317", + "sha256:b91cfe4438c741aeff662d413fd2808ac901cc6229c838236840d11de4586d63", + "sha256:bdb0593a42070b0a5f138b79b872289ee73c8e25b3f0bea6564e795b55b6bcdd", + "sha256:c4e4bca2bb68ce22320297dfa1a7bf070a5b20bcbaec4ee023f83d2f6e76496f", + "sha256:cec4ab14af9eae8501be3266ff50c3c2aecc017ba1e86c160209bb4f0423df6a", + "sha256:e83b4b2bf029f5104bc1227dbb7bf5ace6fd8fabaebffcd4f8106fafc69fc45f", + "sha256:e995b3734a46d41ae60b6097f7c51ba9958648c6d1e0935b7e0ee446ee4abe22", + "sha256:f679d93dec7f7210575c85379a31322df4c46496f184ef650d3aba1484b38a2d", + "sha256:fd213bb5166e46974f113c8228daaef1732abc47cb561ce9c4c8eaed4bd3b09b", + "sha256:fdcb57b906dbc1f80666e6290e794ab8fb959a2e17aa5aee1758a85d1da4533f", + "sha256:ff424b01d090ffe1947ec7432b07f536912e0300458f9a7f48ea217dd8362b86" + ], + "index": "pypi", + "version": "==4.3.3" + }, "maclookup": { "hashes": [ "sha256:33bf8eaebe3b1e4ab4ae9277dd93c78024e0ebf6b3c42f76c37695bc26ce287a", @@ -273,6 +312,11 @@ "index": "pypi", "version": "==1.9.0.post1" }, + "odtreader": { + "editable": true, + "git": "https://github.com/cartertemm/ODTReader.git/", + "ref": "49d6938693f6faa3ff09998f86dba551ae3a996b" + }, "opencv-python": { "hashes": [ "sha256:1703a296a96d3d46615e5053f224867977accb4240bcaa0fcabcb0768bf5ac13", @@ -306,6 +350,40 @@ "index": "pypi", "version": "==4.1.0.25" }, + "pandas": { + "hashes": [ + "sha256:071e42b89b57baa17031af8c6b6bbd2e9a5c68c595bc6bf9adabd7a9ed125d3b", + "sha256:17450e25ae69e2e6b303817bdf26b2cd57f69595d8550a77c308be0cd0fd58fa", + "sha256:17916d818592c9ec891cbef2e90f98cc85e0f1e89ed0924c9b5220dc3209c846", + "sha256:2538f099ab0e9f9c9d09bbcd94b47fd889bad06dc7ae96b1ed583f1dc1a7a822", + "sha256:366f30710172cb45a6b4f43b66c220653b1ea50303fbbd94e50571637ffb9167", + "sha256:42e5ad741a0d09232efbc7fc648226ed93306551772fc8aecc6dce9f0e676794", + "sha256:4e718e7f395ba5bfe8b6f6aaf2ff1c65a09bb77a36af6394621434e7cc813204", + "sha256:4f919f409c433577a501e023943e582c57355d50a724c589e78bc1d551a535a2", + "sha256:4fe0d7e6438212e839fc5010c78b822664f1a824c0d263fd858f44131d9166e2", + "sha256:5149a6db3e74f23dc3f5a216c2c9ae2e12920aa2d4a5b77e44e5b804a5f93248", + "sha256:627594338d6dd995cfc0bacd8e654cd9e1252d2a7c959449228df6740d737eb8", + "sha256:83c702615052f2a0a7fb1dd289726e29ec87a27272d775cb77affe749cca28f8", + "sha256:8c872f7fdf3018b7891e1e3e86c55b190e6c5cee70cab771e8f246c855001296", + "sha256:90f116086063934afd51e61a802a943826d2aac572b2f7d55caaac51c13db5b5", + "sha256:a3352bacac12e1fc646213b998bce586f965c9d431773d9e91db27c7c48a1f7d", + "sha256:bcdd06007cca02d51350f96debe51331dec429ac8f93930a43eb8fb5639e3eb5", + "sha256:c1bd07ebc15285535f61ddd8c0c75d0d6293e80e1ee6d9a8d73f3f36954342d0", + "sha256:c9a4b7c55115eb278c19aa14b34fcf5920c8fe7797a09b7b053ddd6195ea89b3", + "sha256:cc8fc0c7a8d5951dc738f1c1447f71c43734244453616f32b8aa0ef6013a5dfb", + "sha256:d7b460bc316064540ce0c41c1438c416a40746fd8a4fb2999668bf18f3c4acf1" + ], + "index": "pypi", + "version": "==0.24.2" + }, + "pandas-ods-reader": { + "hashes": [ + "sha256:0f7d510639c8957a06aa1227b9f84d1be47a437dfd306464ce803b91cf5eeec4", + "sha256:d85ef58fc3aeac1616028d22954b6ef2e8983ab9bae015e1e90ce3979d138553" + ], + "index": "pypi", + "version": "==0.0.6" + }, "passivetotal": { "hashes": [ "sha256:d745a6519ec04e3a354682978ebf07778bf7602beac30307cbad075ff1a4418d" @@ -313,6 +391,13 @@ "index": "pypi", "version": "==1.0.30" }, + "pdftotext": { + "hashes": [ + "sha256:e3ad11efe0aa22cbfc46aa1296b2ea5a52ad208b778288311f2801adef178ccb" + ], + "index": "pypi", + "version": "==2.1.1" + }, "pillow": { "hashes": [ "sha256:15c056bfa284c30a7f265a41ac4cbbc93bdbfc0dfe0613b9cb8a8581b51a9e55", @@ -447,6 +532,27 @@ ], "version": "==2.8.0" }, + "python-docx": { + "hashes": [ + "sha256:bc76ecac6b2d00ce6442a69d03a6f35c71cd72293cd8405a7472dfe317920024" + ], + "index": "pypi", + "version": "==0.8.10" + }, + "python-pptx": { + "hashes": [ + "sha256:1f2d5d1d923d91f50a1f0ed794935e7d670993fdcb6c12c81cc83977c1f23e14" + ], + "index": "pypi", + "version": "==0.6.17" + }, + "pytz": { + "hashes": [ + "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", + "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141" + ], + "version": "==2019.1" + }, "pyyaml": { "hashes": [ "sha256:1adecc22f88d38052fb787d959f003811ca858b799590a5eaa70e63dca50308c", @@ -642,6 +748,14 @@ "index": "pypi", "version": "==0.5.3" }, + "xlrd": { + "hashes": [ + "sha256:546eb36cee8db40c3eaa46c351e67ffee6eeb5fa2650b71bc4c758a29a1b29b2", + "sha256:e551fb498759fa3a5384a94ccd4c3c02eb7c00ea424426e212ac0c57be9dfbde" + ], + "index": "pypi", + "version": "==1.2.0" + }, "xlsxwriter": { "hashes": [ "sha256:2a40b427dac0f640031e5b33abe97e761de6e0f12d4d346e7b2e2b67cf6ee927", From 980760790f9aa596c87694015b9f90cef2303f15 Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Fri, 26 Apr 2019 13:51:17 +0200 Subject: [PATCH 104/207] chg: [doc] new MISP expansion modules added for PDF, OCR, DOCX, XLSX, PPTX , ODS and ODT. --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 64d0960..cce1923 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ * [Cuckoo submit](misp_modules/modules/expansion/cuckoo_submit.py) - A hover module to submit malware sample, url, attachment, domain to Cuckoo Sandbox. * [DBL Spamhaus](misp_modules/modules/expansion/dbl_spamhaus.py) - a hover module to check Spamhaus DBL for a domain name. * [DNS](misp_modules/modules/expansion/dns.py) - a simple module to resolve MISP attributes like hostname and domain to expand IP addresses attributes. +* [docx-enrich](misp_modules/modules/expansion/docx-enrich.py) - an enrichment module to get text out of Word document into MISP (using free-text parser). * [DomainTools](misp_modules/modules/expansion/domaintools.py) - a hover and expansion module to get information from [DomainTools](http://www.domaintools.com/) whois. * [EUPI](misp_modules/modules/expansion/eupi.py) - a hover and expansion module to get information about an URL from the [Phishing Initiative project](https://phishing-initiative.eu/?lang=en). * [Farsight DNSDB Passive DNS](misp_modules/modules/expansion/farsight_passivedns.py) - a hover and expansion module to expand hostname and IP addresses with passive DNS information. @@ -41,10 +42,15 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ * [iprep](misp_modules/modules/expansion/iprep.py) - an expansion module to get IP reputation from packetmail.net. * [macaddress.io](misp_modules/modules/expansion/macaddress_io.py) - a hover module to retrieve vendor details and other information regarding a given MAC address or an OUI from [MAC address Vendor Lookup](https://macaddress.io). See [integration tutorial here](https://macaddress.io/integrations/MISP-module). * [macvendors](misp_modules/modules/expansion/macvendors.py) - a hover module to retrieve mac vendor information. +* [ocr-enrich](misp_modules/modules/expansion/ocr-enrich.py) - an enrichment module to get OCRized data from images into MISP. +* [ods-enrich](misp_modules/modules/expansion/ods-enrich.py) - an enrichment module to get text out of OpenOffice spreadsheet document into MISP (using free-text parser). +* [odt-enrich](misp_modules/modules/expansion/odt-enrich.py) - an enrichment module to get text out of OpenOffice document into MISP (using free-text parser). * [onyphe](misp_modules/modules/expansion/onyphe.py) - a modules to process queries on Onyphe. * [onyphe_full](misp_modules/modules/expansion/onyphe_full.py) - a modules to process full queries on Onyphe. * [OTX](misp_modules/modules/expansion/otx.py) - an expansion module for [OTX](https://otx.alienvault.com/). * [passivetotal](misp_modules/modules/expansion/passivetotal.py) - a [passivetotal](https://www.passivetotal.org/) module that queries a number of different PassiveTotal datasets. +* [pdf-enrich](misp_modules/modules/expansion/pdf-enrich.py) - an enrichment module to extract text from PDF into MISP (using free-text parser). +* [pptx-enrich](misp_modules/modules/expansion/pptx-enrich.py) - an enrichment module to get text out of PowerPoint document into MISP (using free-text parser). * [qrcode](misp_modules/modules/expansion/qrcode.py) - a module decode QR code, barcode and similar codes from an image and enrich with the decoded values. * [rbl](misp_modules/modules/expansion/rbl.py) - a module to get RBL (Real-Time Blackhost List) values from an attribute. * [reversedns](misp_modules/modules/expansion/reversedns.py) - Simple Reverse DNS expansion service to resolve reverse DNS from MISP attributes. @@ -64,6 +70,7 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ * [whois](misp_modules/modules/expansion) - a module to query a local instance of [uwhois](https://github.com/rafiot/uwhoisd). * [wikidata](misp_modules/modules/expansion/wiki.py) - a [wikidata](https://www.wikidata.org) expansion module. * [xforce](misp_modules/modules/expansion/xforceexchange.py) - an IBM X-Force Exchange expansion module. +* [xlsx-enrich](misp_modules/modules/expansion/xlsx-enrich.py) - an enrichment module to get text out of an Excel document into MISP (using free-text parser). * [YARA query](misp_modules/modules/expansion/yara_query.py) - a module to create YARA rules from single hash attributes. * [YARA syntax validator](misp_modules/modules/expansion/yara_syntax_validator.py) - YARA syntax validator. From d77fdabeb2fab2e7cdf010d8023f1678a6dba3f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Fri, 26 Apr 2019 13:59:36 +0200 Subject: [PATCH 105/207] fix: Re-enable python 3.6 support --- Pipfile | 1 + Pipfile.lock | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 762e4c5..36f059e 100644 --- a/Pipfile +++ b/Pipfile @@ -54,6 +54,7 @@ pandas_ods_reader = "*" pdftotext = "*" lxml = "*" xlrd = "*" +idna-ssl = {markers="python_version < '3.7'"} [requires] python_version = "3" diff --git a/Pipfile.lock b/Pipfile.lock index 6ac6493..fa07b4c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1d4c69ced012268e1ab20cee76f76652e1acd9f5133636551d5686f15748d3b4" + "sha256": "9aac0a9c45df16b9502c13f9468095cf5ffdb8bc407fe2b55faee3ff53d8eba3" }, "pipfile-spec": 6, "requires": { @@ -177,6 +177,14 @@ ], "version": "==2.8" }, + "idna-ssl": { + "hashes": [ + "sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c" + ], + "index": "pypi", + "markers": "python_version < '3.7'", + "version": "==1.1.0" + }, "isodate": { "hashes": [ "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8", From c9281e605da93f7ab9923b798e1e92e4b51fc26a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Fri, 26 Apr 2019 14:05:02 +0200 Subject: [PATCH 106/207] chg: Bump REQUIREMENTS --- REQUIREMENTS | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/REQUIREMENTS b/REQUIREMENTS index 37c5a68..9447c47 100644 --- a/REQUIREMENTS +++ b/REQUIREMENTS @@ -5,6 +5,7 @@ -e git+https://github.com/MISP/PyIntel471.git@0df8d51f1c1425de66714b3a5a45edb69b8cc2fc#egg=pyintel471 -e git+https://github.com/MISP/PyMISP.git@582dda0ce2a8ca8e1dd2cf3842e0491caca51c62#egg=pymisp -e git+https://github.com/Rafiot/uwhoisd.git@411572840eba4c72dc321c549b36a54ed5cea9de#egg=uwhois&subdirectory=client +-e git+https://github.com/cartertemm/ODTReader.git/@49d6938693f6faa3ff09998f86dba551ae3a996b#egg=odtreader -e git+https://github.com/sebdraven/pydnstrails@48c1f740025c51289f43a24863d1845ff12fd21a#egg=pydnstrails -e git+https://github.com/sebdraven/pyonyphe@cbb0168d5cb28a9f71f7ab3773164a7039ccdb12#egg=pyonyphe aiohttp==3.4.4 @@ -23,18 +24,24 @@ dnspython==1.16.0 domaintools-api==0.3.3 enum-compat==0.0.2 ez-setup==0.9 +ezodf==0.3.2 future==0.17.1 httplib2==0.12.3 +idna-ssl==1.1.0 ; python_version < '3.7' idna==2.8 isodate==0.6.0 jsonschema==3.0.1 +lxml==4.3.3 maclookup==1.0.3 multidict==4.5.2 np==1.0.2 numpy==1.16.3 oauth2==1.9.0.post1 opencv-python==4.1.0.25 +pandas-ods-reader==0.0.6 +pandas==0.24.2 passivetotal==1.0.30 +pdftotext==2.1.1 pillow==6.0.0 psutil==5.6.2 pyeupi==1.0 @@ -45,6 +52,9 @@ pypssl==2.1 pyrsistent==0.15.1 pytesseract==0.2.6 python-dateutil==2.8.0 +python-docx==0.8.10 +python-pptx==0.6.17 +pytz==2019.1 pyyaml==5.1 pyzbar==0.1.8 rdflib==4.2.2 @@ -65,6 +75,7 @@ urlarchiver==0.2 urllib3==1.24.2 vulners==1.5.0 wand==0.5.3 +xlrd==1.2.0 xlsxwriter==1.1.7 yara-python==3.8.1 yarl==1.3.0 From c5cbfaedf63d02620f1c142576f035e64f3a1679 Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Sat, 27 Apr 2019 09:08:33 +0200 Subject: [PATCH 107/207] chg: [doc] install of deps updated --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cce1923..6ec5bc6 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ ## How to install and start MISP modules in a Python virtualenv? (recommended) ~~~~bash -sudo apt-get install python3-dev python3-pip libpq5 libjpeg-dev tesseract-ocr imagemagick virtualenv libopencv-dev zbar-tools +sudo apt-get install python3-dev python3-pip libpq5 libjpeg-dev tesseract-ocr libpoppler-cpp-dev imagemagick virtualenv libopencv-dev zbar-tools libzbar0 libzbar-dev libfuzzy-dev -y sudo -u www-data virtualenv -p python3 /var/www/MISP/venv cd /usr/local/src/ sudo git clone https://github.com/MISP/misp-modules.git From 92351e66792c289f327abaace50b955e75bca569 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 1 May 2019 22:22:10 +0200 Subject: [PATCH 108/207] add: Added urlhaus in the expansion modules init list --- misp_modules/modules/expansion/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/__init__.py b/misp_modules/modules/expansion/__init__.py index 70aca68..9c37970 100644 --- a/misp_modules/modules/expansion/__init__.py +++ b/misp_modules/modules/expansion/__init__.py @@ -10,4 +10,4 @@ __all__ = ['cuckoo_submit', 'vmray_submit', 'bgpranking', 'circl_passivedns', 'c 'sigma_queries', 'dbl_spamhaus', 'vulners', 'yara_query', 'macaddress_io', 'intel471', 'backscatter_io', 'btc_scam_check', 'hibp', 'greynoise', 'macvendors', 'qrcode', 'ocr-enrich', 'pdf-enrich', 'docx-enrich', 'xlsx-enrich', 'pptx-enrich', - 'ods-enrich', 'odt-enrich'] + 'ods-enrich', 'odt-enrich', 'urlhaus'] From db74c5f49a1b5b4ed8200637dc32c9a32a75d1fd Mon Sep 17 00:00:00 2001 From: root Date: Wed, 1 May 2019 22:26:53 +0200 Subject: [PATCH 109/207] fix: Fixed libraries import that changed with the latest merge --- misp_modules/modules/import_mod/csvimport.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/misp_modules/modules/import_mod/csvimport.py b/misp_modules/modules/import_mod/csvimport.py index 577229b..2eec1c6 100644 --- a/misp_modules/modules/import_mod/csvimport.py +++ b/misp_modules/modules/import_mod/csvimport.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +from collections import defaultdict +import csv +import io import json import os import base64 @@ -162,7 +165,7 @@ class CsvParser(): self.result.append(attribute) def findMispTypes(self): - descFilename = os.path.join(pymisp_path[0], 'data/describeTypes.json') + descFilename = os.path.join(pymisp.__path__[0], 'data/describeTypes.json') with open(descFilename, 'r') as f: MispTypes = json.loads(f.read())['result'].get('types') list2pop = [] From f900cb7c68837204714f85e2e3e6c9e04042c794 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 1 May 2019 22:28:19 +0200 Subject: [PATCH 110/207] fix: Fixed introspection fields for csvimport & goamlimport - Added format field for goaml so the module is known as returning MISP attributes & objects - Fixed introspection to make the format, user config and input source fields visible from MISP (format also added at the same time) --- misp_modules/modules/import_mod/csvimport.py | 15 ++------------- misp_modules/modules/import_mod/goamlimport.py | 3 ++- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/misp_modules/modules/import_mod/csvimport.py b/misp_modules/modules/import_mod/csvimport.py index 2eec1c6..bb86014 100644 --- a/misp_modules/modules/import_mod/csvimport.py +++ b/misp_modules/modules/import_mod/csvimport.py @@ -12,7 +12,6 @@ moduleinfo = {'version': '0.1', 'author': 'Christian Studer', 'description': 'Import Attributes from a csv file.', 'module-type': ['import']} moduleconfig = [] -inputSource = ['file'] userConfig = {'header': { 'type': 'String', 'message': 'Define the header of the csv file, with types (included in MISP attribute types or attribute fields) separated by commas.\nFor fields that do not match these types, please use space or simply nothing between commas.\nFor instance: ip-src,domain, ,timestamp'}, @@ -20,6 +19,7 @@ userConfig = {'header': { 'type': 'Boolean', 'message': 'Tick this box ONLY if there is a header line, NOT COMMENTED, in the file (which will be skipped atm).' }} +mispattributes = {'userConfig': userConfig, 'inputSource': ['file'], 'format': 'misp_standard'} duplicatedFields = {'mispType': {'mispComment': 'comment'}, 'attrField': {'attrComment': 'comment'}} @@ -224,18 +224,7 @@ def handler(q=False): def introspection(): - modulesetup = {} - try: - userConfig - modulesetup['userConfig'] = userConfig - except NameError: - pass - try: - inputSource - modulesetup['inputSource'] = inputSource - except NameError: - pass - return modulesetup + return mispattributes def version(): diff --git a/misp_modules/modules/import_mod/goamlimport.py b/misp_modules/modules/import_mod/goamlimport.py index 7116b44..add6470 100644 --- a/misp_modules/modules/import_mod/goamlimport.py +++ b/misp_modules/modules/import_mod/goamlimport.py @@ -9,7 +9,8 @@ moduleinfo = {'version': 1, 'author': 'Christian Studer', 'description': 'Import from GoAML', 'module-type': ['import']} moduleconfig = [] -mispattributes = {'inputSource': ['file'], 'output': ['MISP objects']} +mispattributes = {'inputSource': ['file'], 'output': ['MISP objects'], + 'format': 'misp_standard'} t_from_objects = {'nodes': ['from_person', 'from_account', 'from_entity'], 'leaves': ['from_funds_code', 'from_country']} From c886247a649c3b6f70463a009d4d1960824378b8 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 1 May 2019 22:32:06 +0200 Subject: [PATCH 111/207] fix: Fixed standard MISP csv format header - The csv header we can find in data produced from MISP restSearch csv format is the one to use to recognize a csv file produced by MISP --- misp_modules/modules/import_mod/csvimport.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/misp_modules/modules/import_mod/csvimport.py b/misp_modules/modules/import_mod/csvimport.py index bb86014..ec55a10 100644 --- a/misp_modules/modules/import_mod/csvimport.py +++ b/misp_modules/modules/import_mod/csvimport.py @@ -24,11 +24,11 @@ mispattributes = {'userConfig': userConfig, 'inputSource': ['file'], 'format': ' duplicatedFields = {'mispType': {'mispComment': 'comment'}, 'attrField': {'attrComment': 'comment'}} attributesFields = ['type', 'value', 'category', 'to_ids', 'comment', 'distribution'] -misp_standard_csv_header = ['uuid','event_id','category','type','value','comment','to_ids','date', - 'object_relation','object_uuid','object_name','object_meta_category'] +misp_standard_csv_header = ['uuid', 'event_id', 'category', 'type', 'value', 'comment', 'to_ids', 'date', + 'object_relation', 'attribute_tag', 'object_uuid', 'object_name', 'object_meta_category'] misp_context_additional_fields = ['event_info','event_member_org','event_source_org','event_distribution', 'event_threat_level_id','event_analysis','event_date','event_tag'] -misp_extended_csv_header = misp_standard_csv_header[:9] + ['attribute_tag'] + misp_standard_csv_header[9:] + misp_context_additional_fields +misp_extended_csv_header = misp_standard_csv_header + misp_context_additional_fields delimiters = [',', ';', '|', '/', '\t', ' '] From c8a4d8d76f2cc6057f0230121c5020a17fc3710b Mon Sep 17 00:00:00 2001 From: Koen Van Impe Date: Wed, 1 May 2019 22:44:24 +0200 Subject: [PATCH 112/207] New VMRay modules New JSON output format of VMRay Prepare for automation (via PyMISP) with workflow taxonomy tags --- .../modules/expansion/vmray_submit.py | 23 +- .../modules/import_mod/vmray_import.py | 432 ++++++++++-------- 2 files changed, 261 insertions(+), 194 deletions(-) diff --git a/misp_modules/modules/expansion/vmray_submit.py b/misp_modules/modules/expansion/vmray_submit.py index 062c21f..9b1c1a5 100644 --- a/misp_modules/modules/expansion/vmray_submit.py +++ b/misp_modules/modules/expansion/vmray_submit.py @@ -3,10 +3,12 @@ ''' Submit sample to VMRay. -Submit a sample to VMRay +Requires "vmray_rest_api" -TODO: - # Deal with archive submissions +The expansion module vmray_submit and import module vmray_import are a two step +process to import data from VMRay. +You can automate this by setting the PyMISP example script 'vmray_automation' +as a cron job ''' @@ -129,13 +131,13 @@ def vmrayProcess(vmraydata): # Result received? if submissions and jobs: r = {'results': []} - r["results"].append({"types": "md5", "values": submissions["submission_sample_md5"]}) - r["results"].append({"types": "sha1", "values": submissions["submission_sample_sha1"]}) - r["results"].append({"types": "sha256", "values": submissions["submission_sample_sha256"]}) - r["results"].append({"types": "text", "values": "VMRay Sample ID: %s" % submissions["submission_sample_id"]}) - r["results"].append({"types": "text", "values": "VMRay Submission ID: %s" % submissions["submission_id"]}) - r["results"].append({"types": "text", "values": "VMRay Submission Sample IP: %s" % submissions["submission_ip_ip"]}) - r["results"].append({"types": "link", "values": submissions["submission_webif_url"]}) + r['results'].append({'types': 'md5', 'values': submissions['submission_sample_md5']}) + r['results'].append({'types': 'sha1', 'values': submissions['submission_sample_sha1']}) + r['results'].append({'types': 'sha256', 'values': submissions['submission_sample_sha256']}) + r['results'].append({'types': 'text', 'values': 'VMRay Sample ID: %s' % submissions['submission_sample_id'], 'tags': 'workflow:state="incomplete"' }) + r['results'].append({'types': 'text', 'values': 'VMRay Submission ID: %s' % submissions['submission_id']}) + r['results'].append({'types': 'text', 'values': 'VMRay Submission Sample IP: %s' % submissions['submission_ip_ip']}) + r['results'].append({'types': 'link', 'values': submissions['submission_webif_url']}) # Include data from different jobs if include_vmrayjobids: @@ -160,3 +162,4 @@ def vmraySubmit(api, args): ''' Submit the sample to VMRay''' vmraydata = api.call("POST", "/rest/sample/submit", args) return vmraydata + diff --git a/misp_modules/modules/import_mod/vmray_import.py b/misp_modules/modules/import_mod/vmray_import.py index 819ea66..6adf7a6 100644 --- a/misp_modules/modules/import_mod/vmray_import.py +++ b/misp_modules/modules/import_mod/vmray_import.py @@ -8,9 +8,10 @@ This version supports import from different analyze jobs, starting from one samp Requires "vmray_rest_api" -TODO: - # Import one job (analyze_id) - # Import STIX package (XML version) +The expansion module vmray_submit and import module vmray_import are a two step +process to import data from VMRay. +You can automate this by setting the PyMISP example script 'vmray_automation' +as a cron job ''' @@ -21,55 +22,49 @@ from ._vmray.vmray_rest_api import VMRayRESTAPI misperrors = {'error': 'Error'} inputSource = [] -moduleinfo = {'version': '0.1', 'author': 'Koen Van Impe', - 'description': 'Import VMRay (VTI) results', +moduleinfo = {'version': '0.2', 'author': 'Koen Van Impe', + 'description': 'Import VMRay results', 'module-type': ['import']} -userConfig = {'include_textdescr': {'type': 'Boolean', - 'message': 'Include textual description' - }, +userConfig = { 'include_analysisid': {'type': 'Boolean', - 'message': 'Include VMRay analysis_id text' + 'message': 'Include link to VMRay analysis' }, - 'only_network_info': {'type': 'Boolean', - 'message': 'Only include network (src-ip, hostname, domain, ...) information' - }, + 'include_analysisdetails': {'type': 'Boolean', + 'message': 'Include (textual) analysis details' + }, + 'include_vtidetails': {'type': 'Boolean', + 'message': 'Include VMRay Threat Identifier (VTI) rules' + }, + 'include_imphash_ssdeep': {'type': 'Boolean', + 'message': 'Include imphash and ssdeep' + }, + 'include_extracted_files': {'type': 'Boolean', + 'message': 'Include extracted files section' + }, + 'sample_id': {'type': 'Integer', 'errorMessage': 'Expected a sample ID', 'message': 'The VMRay sample_id' } } -moduleconfig = ['apikey', 'url'] - -include_textdescr = False -include_analysisid = False -only_network_info = False - +moduleconfig = ['apikey', 'url', 'wait_period'] def handler(q=False): - global include_textdescr - global include_analysisid - global only_network_info + global include_analysisid, include_imphash_ssdeep, include_extracted_files, include_analysisdetails, include_vtidetails, include_static_to_ids if q is False: return False request = json.loads(q) - include_textdescr = request["config"].get("include_textdescr") - include_analysisid = request["config"].get("include_analysisid") - only_network_info = request["config"].get("only_network_info") - if include_textdescr == "1": - include_textdescr = True - else: - include_textdescr = False - if include_analysisid == "1": - include_analysisid = True - else: - include_analysisid = False - if only_network_info == "1": - only_network_info = True - else: - only_network_info = False + include_analysisid = bool(int(request["config"].get("include_analysisid"))) + include_imphash_ssdeep = bool(int(request["config"].get("include_imphash_ssdeep"))) + include_extracted_files = bool(int(request["config"].get("include_extracted_files"))) + include_analysisdetails = bool(int(request["config"].get("include_extracted_files"))) + include_vtidetails = bool(int(request["config"].get("include_vtidetails"))) + include_static_to_ids = True + + #print("include_analysisid: %s include_imphash_ssdeep: %s include_extracted_files: %s include_analysisdetails: %s include_vtidetails: %s" % ( include_analysisid, include_imphash_ssdeep, include_extracted_files, include_analysisdetails, include_vtidetails)) sample_id = int(request["config"].get("sample_id")) @@ -81,34 +76,52 @@ def handler(q=False): try: api = VMRayRESTAPI(request["config"].get("url"), request["config"].get("apikey"), False) vmray_results = {'results': []} + # Get all information on the sample, returns a set of finished analyze jobs data = vmrayGetInfoAnalysis(api, sample_id) if data["data"]: - vti_patterns_found = False for analysis in data["data"]: - analysis_id = analysis["analysis_id"] - + analysis_id = int(analysis["analysis_id"]) if analysis_id > 0: # Get the details for an analyze job analysis_data = vmrayDownloadAnalysis(api, analysis_id) if analysis_data: - if "analysis_vti_patterns" in analysis_data: - p = vmrayVtiPatterns(analysis_data["analysis_vti_patterns"]) - else: - p = vmrayVtiPatterns(analysis_data["vti_patterns"]) - if p and len(p["results"]) > 0: - vti_patterns_found = True - vmray_results = {'results': vmray_results["results"] + p["results"]} + if include_analysisdetails and "analysis_details" in analysis_data: + analysis_details = vmrayAnalysisDetails(analysis_data["analysis_details"], analysis_id) + if analysis_details and len(analysis_details["results"]) > 0: + vmray_results = {'results': vmray_results["results"] + analysis_details["results"]} + + if "classifications" in analysis_data: + classifications = vmrayClassifications(analysis_data["classifications"], analysis_id) + if classifications and len(classifications["results"]) > 0: + vmray_results = {'results': vmray_results["results"] + classifications["results"]} + + if include_extracted_files and "extracted_files" in analysis_data: + extracted_files = vmrayExtractedfiles(analysis_data["extracted_files"]) + if extracted_files and len(extracted_files["results"]) > 0: + vmray_results = {'results': vmray_results["results"] + extracted_files["results"]} + + if include_vtidetails and "vti" in analysis_data: + vti = vmrayVti(analysis_data["vti"]) + if vti and len(vti["results"]) > 0: + vmray_results = {'results': vmray_results["results"] + vti["results"]} + + if "artifacts" in analysis_data: + artifacts = vmrayArtifacts(analysis_data["artifacts"]) + if artifacts and len(artifacts["results"]) > 0: + vmray_results = {'results': vmray_results["results"] + artifacts["results"]} + if include_analysisid: a_id = {'results': []} - url1 = "https://cloud.vmray.com/user/analysis/view?from_sample_id=%u" % sample_id + url1 = request["config"].get("url") + "/user/analysis/view?from_sample_id=%u" % sample_id url2 = "&id=%u" % analysis_id url3 = "&sub=%2Freport%2Foverview.html" a_id["results"].append({"values": url1 + url2 + url3, "types": "link"}) vmray_results = {'results': vmray_results["results"] + a_id["results"]} + # Clean up (remove doubles) - if vti_patterns_found: + if len(vmray_results["results"]) > 0: vmray_results = vmrayCleanup(vmray_results) return vmray_results else: @@ -117,8 +130,8 @@ def handler(q=False): else: misperrors['error'] = "Unable to fetch sample id %u" % (sample_id) return misperrors - except Exception: - misperrors['error'] = "Unable to access VMRay API" + except Exception as e: + misperrors['error'] = "Unable to access VMRay API : %s" % (e) return misperrors else: misperrors['error'] = "Not a valid sample id" @@ -158,165 +171,216 @@ def vmrayGetInfoAnalysis(api, sample_id): def vmrayDownloadAnalysis(api, analysis_id): ''' Get the details from an analysis''' if analysis_id: - data = api.call("GET", "/rest/analysis/%u/archive/additional/vti_result.json" % (analysis_id), raw_data=True) - return json.loads(data.read().decode()) + try: + data = api.call("GET", "/rest/analysis/%u/archive/logs/summary.json" % (analysis_id), raw_data=True) + return json.loads(data.read().decode()) + except Exception as e: + misperrors['error'] = "Unable to download summary.json for analysis %s" % (analysis_id) + return misperrors + else: + return False + +def vmrayVti(vti): + '''VMRay Threat Identifier (VTI) rules that matched for this analysis''' + + if vti: + r = {'results': []} + for rule in vti: + if rule == "vti_rule_matches": + vti_rule = vti["vti_rule_matches"] + for el in vti_rule: + if "operation_desc" in el: + comment = "" + types = ["text"] + values = el["operation_desc"] + r['results'].append({'types': types, 'values': values, 'comment': comment}) + + return r + else: return False -def vmrayVtiPatterns(vti_patterns): - ''' Match the VTI patterns to MISP data''' +def vmrayExtractedfiles(extracted_files): + ''' Information about files which were extracted during the analysis, such as files that were created, modified, or embedded by the malware''' - if vti_patterns: + if extracted_files: + r = {'results': []} + + for file in extracted_files: + if "file_type" and "norm_filename" in file: + comment = "%s - %s" % (file["file_type"], file["norm_filename"]) + else: + comment = "" + + if "norm_filename" in file: + attr_filename_c = file["norm_filename"].rsplit("\\",1) + if len(attr_filename_c) > 1: + attr_filename = attr_filename_c[len(attr_filename_c) - 1] + else: + attr_filename = "vmray_sample" + else: + attr_filename = "vmray_sample" + + if "md5_hash" in file and file["md5_hash"] is not None: + r['results'].append({'types': ["filename|md5"], 'values': '{}|{}'.format(attr_filename,file["md5_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) + if include_imphash_ssdeep and "imp_hash" in file and file["imp_hash"] is not None: + r['results'].append({'types': ["filename|imphash"], 'values': '{}|{}'.format(attr_filename,file["imp_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) + if "sha1_hash" in file and file["sha1_hash"] is not None: + r['results'].append({'types': ["filename|sha1"], 'values': '{}|{}'.format(attr_filename,file["sha1_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) + if "sha256_hash" in file and file["sha256_hash"] is not None: + r['results'].append({'types': ["filename|sha256"], 'values': '{}|{}'.format(attr_filename,file["sha256_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) + if include_imphash_ssdeep and "ssdeep_hash" in file and file["ssdeep_hash"] is not None: + r['results'].append({'types': ["filename|ssdeep"], 'values': '{}|{}'.format(attr_filename,file["ssdeep_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) + + return r + + else: + return False + + +def vmrayClassifications(classification, analysis_id): + ''' List the classifications, tag them on a "text" attribute ''' + + if classification: + r = {'results': []} + types = ["text"] + comment = "" + values = "Classification : %s " % (", ".join(str(x) for x in classification)) + r['results'].append({'types': types, 'values': values, 'comment': comment}) + + return r + + else: + return False + + +def vmrayAnalysisDetails(details, analysis_id): + ''' General information about the analysis information ''' + + if details: + r = {'results': []} + types = ["text"] + comment = "" + if "execution_successful" in details: + values = "Analysis %s : execution_successful : %s " % (analysis_id, str(details["execution_successful"])) + r['results'].append({'types': types, 'values': values, 'comment': comment}) + if "termination_reason" in details: + values = "Analysis %s : termination_reason : %s " % (analysis_id, str(details["termination_reason"])) + r['results'].append({'types': types, 'values': values, 'comment': comment}) + if "result_str" in details: + values = "Analysis %s : result : %s " % (analysis_id, details["result_str"]) + r['results'].append({'types': types, 'values': values, 'comment': comment}) + + return r + + else: + return false + + +def vmrayArtifacts(patterns): + ''' IOCs that were seen during the analysis ''' + + if patterns: r = {'results': []} y = {'results': []} - for pattern in vti_patterns: - content = False - if pattern["category"] == "_network" and pattern["operation"] == "_download_data": - content = vmrayGeneric(pattern, "url", 1) - elif pattern["category"] == "_network" and pattern["operation"] == "_connect": - content = vmrayConnect(pattern) - elif pattern["category"] == "_network" and pattern["operation"] == "_install_server": - content = vmrayGeneric(pattern) + for pattern in patterns: + if pattern == "domains": + for el in patterns[pattern]: + values = el["domain"] + types = ["domain", "hostname"] + if "sources" in el: + sources = el["sources"] + comment = "Found in: " + ", ".join(str(x) for x in sources) + else: + comment = "" + r['results'].append({'types': types, 'values': values, 'comment': comment, 'to_ids': include_static_to_ids}) + if pattern == "files": + for el in patterns[pattern]: + filename_values = el["filename"] + attr_filename_c = filename_values.rsplit("\\",1) + if len(attr_filename_c) > 1: + attr_filename = attr_filename_c[len(attr_filename_c) - 1] + else: + attr_filename = "" + filename_types = ["filename"] + filename_operations = el["operations"] + comment = "File operations: " + ", ".join(str(x) for x in filename_operations) + r['results'].append({'types': filename_types, 'values': filename_values, 'comment': comment}) - elif only_network_info is False and pattern["category"] == "_process" and pattern["operation"] == "_alloc_wx_page": - content = vmrayGeneric(pattern) - elif only_network_info is False and pattern["category"] == "_process" and pattern["operation"] == "_install_ipc_endpoint": - content = vmrayGeneric(pattern, "mutex", 1) - elif only_network_info is False and pattern["category"] == "_process" and pattern["operation"] == "_crashed_process": - content = vmrayGeneric(pattern) - elif only_network_info is False and pattern["category"] == "_process" and pattern["operation"] == "_read_from_remote_process": - content = vmrayGeneric(pattern) - elif only_network_info is False and pattern["category"] == "_process" and pattern["operation"] == "_create_process_with_hidden_window": - content = vmrayGeneric(pattern) + # Run through all hashes + if "hashes" in el: + for hash in el["hashes"]: + if "md5_hash" in hash and hash["md5_hash"] is not None: + r['results'].append({'types': ["filename|md5"], 'values': '{}|{}'.format(attr_filename,hash["md5_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) + if include_imphash_ssdeep and "imp_hash" in hash and hash["imp_hash"] is not None: + r['results'].append({'types': ["filename|imphash"], 'values': '{}|{}'.format(attr_filename,hash["imp_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) + if "sha1_hash" in hash and hash["sha1_hash"] is not None: + r['results'].append({'types': ["filename|sha1"], 'values': '{}|{}'.format(attr_filename,hash["sha1_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) + if "sha256_hash" in hash and hash["sha256_hash"] is not None: + r['results'].append({'types': ["filename|sha256"], 'values': '{}|{}'.format(attr_filename,hash["sha256_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) + if include_imphash_ssdeep and "ssdeep_hash" in hash and hash["ssdeep_hash"] is not None: + r['results'].append({'types': ["filename|ssdeep"], 'values': '{}|{}'.format(attr_filename,hash["ssdeep_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) + if pattern == "ips": + for el in patterns[pattern]: + values = el["ip_address"] + types = ["ip-dst"] + if "sources" in el: + sources = el["sources"] + comment = "Found in: " + ", ".join(str(x) for x in sources) + else: + comment = "" - elif only_network_info is False and pattern["category"] == "_anti_analysis" and pattern["operation"] == "_delay_execution": - content = vmrayGeneric(pattern) - elif only_network_info is False and pattern["category"] == "_anti_analysis" and pattern["operation"] == "_dynamic_api_usage": - content = vmrayGeneric(pattern) + r['results'].append({'types': types, 'values': values, 'comment': comment, 'to_ids': include_static_to_ids}) + if pattern == "mutexes": + for el in patterns[pattern]: + values = el["mutex_name"] + types = ["mutex"] + if "sources" in el: + sources = el["operations"] + comment = "Operations: " + ", ".join(str(x) for x in sources) + else: + comment = "" - elif only_network_info is False and pattern["category"] == "_static" and pattern["operation"] == "_drop_pe_file": - content = vmrayGeneric(pattern, "filename", 1) - elif only_network_info is False and pattern["category"] == "_static" and pattern["operation"] == "_execute_dropped_pe_file": - content = vmrayGeneric(pattern, "filename", 1) + r['results'].append({'types': types, 'values': values, 'comment': comment, 'to_ids': include_static_to_ids}) + if pattern == "registry": + for el in patterns[pattern]: + values = el["reg_key_name"] + types = ["regkey"] + if "sources" in el: + sources = el["operations"] + comment = "Operations: " + ", ".join(str(x) for x in sources) + else: + comment = "" - elif only_network_info is False and pattern["category"] == "_injection" and pattern["operation"] == "_modify_memory": - content = vmrayGeneric(pattern) - elif only_network_info is False and pattern["category"] == "_injection" and pattern["operation"] == "_modify_memory_system": - content = vmrayGeneric(pattern) - elif only_network_info is False and pattern["category"] == "_injection" and pattern["operation"] == "_modify_memory_non_system": - content = vmrayGeneric(pattern) - elif only_network_info is False and pattern["category"] == "_injection" and pattern["operation"] == "_modify_control_flow": - content = vmrayGeneric(pattern) - elif only_network_info is False and pattern["category"] == "_injection" and pattern["operation"] == "_modify_control_flow_non_system": - content = vmrayGeneric(pattern) - elif only_network_info is False and pattern["category"] == "_file_system" and pattern["operation"] == "_create_many_files": - content = vmrayGeneric(pattern) + r['results'].append({'types': types, 'values': values, 'comment': comment, 'to_ids': include_static_to_ids}) + if pattern == "urls": + for el in patterns[pattern]: + values = el["url"] + types = ["url"] + if "sources" in el: + sources = el["operations"] + comment = "Operations: " + ", ".join(str(x) for x in sources) + else: + comment = "" - elif only_network_info is False and pattern["category"] == "_hide_tracks" and pattern["operation"] == "_hide_data_in_registry": - content = vmrayGeneric(pattern, "regkey", 1) + r['results'].append({'types': types, 'values': values, 'comment': comment, 'to_ids': include_static_to_ids}) - elif only_network_info is False and pattern["category"] == "_persistence" and pattern["operation"] == "_install_startup_script": - content = vmrayGeneric(pattern, "regkey", 1) - elif only_network_info is False and pattern["category"] == "_os" and pattern["operation"] == "_enable_process_privileges": - content = vmrayGeneric(pattern) - - if content: - r["results"].append(content["attributes"]) - r["results"].append(content["text"]) - - # Remove empty results - r["results"] = [x for x in r["results"] if isinstance(x, dict) and len(x["values"]) != 0] + # Remove doubles for el in r["results"]: if el not in y["results"]: y["results"].append(el) return y + else: - return False + return false def vmrayCleanup(x): ''' Remove doubles''' y = {'results': []} - for el in x["results"]: if el not in y["results"]: y["results"].append(el) return y - - -def vmraySanitizeInput(s): - ''' Sanitize some input so it gets properly imported in MISP''' - if s: - s = s.replace('"', '') - s = re.sub('\\\\', r'\\', s) - return s - else: - return False - - -def vmrayGeneric(el, attr="", attrpos=1): - ''' Convert a 'generic' VTI pattern to MISP data''' - - r = {"values": []} - f = {"values": []} - - if el: - content = el["technique_desc"] - if content: - if attr: - # Some elements are put between \"\" ; replace them to single - content = content.replace("\"\"", "\"") - content_split = content.split("\"") - # Attributes are between open " and close "; so use > - if len(content_split) > attrpos: - content_split[attrpos] = vmraySanitizeInput(content_split[attrpos]) - r["values"].append(content_split[attrpos]) - r["types"] = [attr] - - # Adding the value also as text to get the extra description, - # but this is pretty useless for "url" - if include_textdescr and attr != "url": - f["values"].append(vmraySanitizeInput(content)) - f["types"] = ["text"] - - return {"text": f, "attributes": r} - else: - return False - else: - return False - - -def vmrayConnect(el): - ''' Extension of vmrayGeneric , parse network connect data''' - ipre = re.compile("([0-9]{1,3}.){3}[0-9]{1,3}") - - r = {"values": []} - f = {"values": []} - - if el: - content = el["technique_desc"] - if content: - target = content.split("\"") - # port = (target[1].split(":"))[1] ## FIXME: not used - host = (target[1].split(":"))[0] - if ipre.match(str(host)): - r["values"].append(host) - r["types"] = ["ip-dst"] - else: - r["values"].append(host) - r["types"] = ["domain", "hostname"] - - f["values"].append(vmraySanitizeInput(target[1])) - f["types"] = ["text"] - - if include_textdescr: - f["values"].append(vmraySanitizeInput(content)) - f["types"] = ["text"] - - return {"text": f, "attributes": r} - else: - return False - else: - return False From 553cf44337564751a654fdf4088ef2f44a27a55d Mon Sep 17 00:00:00 2001 From: Steve Clement Date: Thu, 2 May 2019 10:37:48 +0900 Subject: [PATCH 113/207] fix: [pep8] Fixes --- misp_modules/modules/expansion/vmray_submit.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/misp_modules/modules/expansion/vmray_submit.py b/misp_modules/modules/expansion/vmray_submit.py index 9b1c1a5..4d34c4b 100644 --- a/misp_modules/modules/expansion/vmray_submit.py +++ b/misp_modules/modules/expansion/vmray_submit.py @@ -134,7 +134,7 @@ def vmrayProcess(vmraydata): r['results'].append({'types': 'md5', 'values': submissions['submission_sample_md5']}) r['results'].append({'types': 'sha1', 'values': submissions['submission_sample_sha1']}) r['results'].append({'types': 'sha256', 'values': submissions['submission_sample_sha256']}) - r['results'].append({'types': 'text', 'values': 'VMRay Sample ID: %s' % submissions['submission_sample_id'], 'tags': 'workflow:state="incomplete"' }) + r['results'].append({'types': 'text', 'values': 'VMRay Sample ID: %s' % submissions['submission_sample_id'], 'tags': 'workflow:state="incomplete"'}) r['results'].append({'types': 'text', 'values': 'VMRay Submission ID: %s' % submissions['submission_id']}) r['results'].append({'types': 'text', 'values': 'VMRay Submission Sample IP: %s' % submissions['submission_ip_ip']}) r['results'].append({'types': 'link', 'values': submissions['submission_webif_url']}) @@ -162,4 +162,3 @@ def vmraySubmit(api, args): ''' Submit the sample to VMRay''' vmraydata = api.call("POST", "/rest/sample/submit", args) return vmraydata - From 81ffabd62104f2e40874fe3fbf47176402d7f6f0 Mon Sep 17 00:00:00 2001 From: Steve Clement Date: Thu, 2 May 2019 11:06:32 +0900 Subject: [PATCH 114/207] fix: [pep8] More pep8 happiness --- .../modules/import_mod/vmray_import.py | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/misp_modules/modules/import_mod/vmray_import.py b/misp_modules/modules/import_mod/vmray_import.py index 6adf7a6..068c820 100644 --- a/misp_modules/modules/import_mod/vmray_import.py +++ b/misp_modules/modules/import_mod/vmray_import.py @@ -16,7 +16,6 @@ as a cron job ''' import json -import re from ._vmray.vmray_rest_api import VMRayRESTAPI @@ -25,34 +24,34 @@ inputSource = [] moduleinfo = {'version': '0.2', 'author': 'Koen Van Impe', 'description': 'Import VMRay results', 'module-type': ['import']} -userConfig = { - 'include_analysisid': {'type': 'Boolean', +userConfig = {'include_analysisid': {'type': 'Boolean', 'message': 'Include link to VMRay analysis' - }, + }, 'include_analysisdetails': {'type': 'Boolean', - 'message': 'Include (textual) analysis details' - }, + 'message': 'Include (textual) analysis details' + }, 'include_vtidetails': {'type': 'Boolean', 'message': 'Include VMRay Threat Identifier (VTI) rules' - }, + }, 'include_imphash_ssdeep': {'type': 'Boolean', 'message': 'Include imphash and ssdeep' }, 'include_extracted_files': {'type': 'Boolean', - 'message': 'Include extracted files section' - }, + 'message': 'Include extracted files section' + }, 'sample_id': {'type': 'Integer', 'errorMessage': 'Expected a sample ID', 'message': 'The VMRay sample_id' } - } + } moduleconfig = ['apikey', 'url', 'wait_period'] def handler(q=False): global include_analysisid, include_imphash_ssdeep, include_extracted_files, include_analysisdetails, include_vtidetails, include_static_to_ids + if q is False: return False request = json.loads(q) @@ -64,7 +63,7 @@ def handler(q=False): include_vtidetails = bool(int(request["config"].get("include_vtidetails"))) include_static_to_ids = True - #print("include_analysisid: %s include_imphash_ssdeep: %s include_extracted_files: %s include_analysisdetails: %s include_vtidetails: %s" % ( include_analysisid, include_imphash_ssdeep, include_extracted_files, include_analysisdetails, include_vtidetails)) + # print("include_analysisid: %s include_imphash_ssdeep: %s include_extracted_files: %s include_analysisdetails: %s include_vtidetails: %s" % ( include_analysisid, include_imphash_ssdeep, include_extracted_files, include_analysisdetails, include_vtidetails)) sample_id = int(request["config"].get("sample_id")) @@ -183,6 +182,7 @@ def vmrayDownloadAnalysis(api, analysis_id): def vmrayVti(vti): '''VMRay Threat Identifier (VTI) rules that matched for this analysis''' + if vti: r = {'results': []} for rule in vti: @@ -214,7 +214,7 @@ def vmrayExtractedfiles(extracted_files): comment = "" if "norm_filename" in file: - attr_filename_c = file["norm_filename"].rsplit("\\",1) + attr_filename_c = file["norm_filename"].rsplit("\\", 1) if len(attr_filename_c) > 1: attr_filename = attr_filename_c[len(attr_filename_c) - 1] else: @@ -223,15 +223,15 @@ def vmrayExtractedfiles(extracted_files): attr_filename = "vmray_sample" if "md5_hash" in file and file["md5_hash"] is not None: - r['results'].append({'types': ["filename|md5"], 'values': '{}|{}'.format(attr_filename,file["md5_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) + r['results'].append({'types': ["filename|md5"], 'values': '{}|{}'.format(attr_filename, file["md5_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) if include_imphash_ssdeep and "imp_hash" in file and file["imp_hash"] is not None: - r['results'].append({'types': ["filename|imphash"], 'values': '{}|{}'.format(attr_filename,file["imp_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) + r['results'].append({'types': ["filename|imphash"], 'values': '{}|{}'.format(attr_filename, file["imp_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) if "sha1_hash" in file and file["sha1_hash"] is not None: - r['results'].append({'types': ["filename|sha1"], 'values': '{}|{}'.format(attr_filename,file["sha1_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) + r['results'].append({'types': ["filename|sha1"], 'values': '{}|{}'.format(attr_filename, file["sha1_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) if "sha256_hash" in file and file["sha256_hash"] is not None: - r['results'].append({'types': ["filename|sha256"], 'values': '{}|{}'.format(attr_filename,file["sha256_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) + r['results'].append({'types': ["filename|sha256"], 'values': '{}|{}'.format(attr_filename, file["sha256_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) if include_imphash_ssdeep and "ssdeep_hash" in file and file["ssdeep_hash"] is not None: - r['results'].append({'types': ["filename|ssdeep"], 'values': '{}|{}'.format(attr_filename,file["ssdeep_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) + r['results'].append({'types': ["filename|ssdeep"], 'values': '{}|{}'.format(attr_filename, file["ssdeep_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) return r @@ -275,7 +275,7 @@ def vmrayAnalysisDetails(details, analysis_id): return r else: - return false + return False def vmrayArtifacts(patterns): @@ -299,7 +299,7 @@ def vmrayArtifacts(patterns): if pattern == "files": for el in patterns[pattern]: filename_values = el["filename"] - attr_filename_c = filename_values.rsplit("\\",1) + attr_filename_c = filename_values.rsplit("\\", 1) if len(attr_filename_c) > 1: attr_filename = attr_filename_c[len(attr_filename_c) - 1] else: @@ -313,15 +313,15 @@ def vmrayArtifacts(patterns): if "hashes" in el: for hash in el["hashes"]: if "md5_hash" in hash and hash["md5_hash"] is not None: - r['results'].append({'types': ["filename|md5"], 'values': '{}|{}'.format(attr_filename,hash["md5_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) + r['results'].append({'types': ["filename|md5"], 'values': '{}|{}'.format(attr_filename, hash["md5_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) if include_imphash_ssdeep and "imp_hash" in hash and hash["imp_hash"] is not None: - r['results'].append({'types': ["filename|imphash"], 'values': '{}|{}'.format(attr_filename,hash["imp_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) + r['results'].append({'types': ["filename|imphash"], 'values': '{}|{}'.format(attr_filename, hash["imp_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) if "sha1_hash" in hash and hash["sha1_hash"] is not None: - r['results'].append({'types': ["filename|sha1"], 'values': '{}|{}'.format(attr_filename,hash["sha1_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) + r['results'].append({'types': ["filename|sha1"], 'values': '{}|{}'.format(attr_filename, hash["sha1_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) if "sha256_hash" in hash and hash["sha256_hash"] is not None: - r['results'].append({'types': ["filename|sha256"], 'values': '{}|{}'.format(attr_filename,hash["sha256_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) + r['results'].append({'types': ["filename|sha256"], 'values': '{}|{}'.format(attr_filename, hash["sha256_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) if include_imphash_ssdeep and "ssdeep_hash" in hash and hash["ssdeep_hash"] is not None: - r['results'].append({'types': ["filename|ssdeep"], 'values': '{}|{}'.format(attr_filename,hash["ssdeep_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) + r['results'].append({'types': ["filename|ssdeep"], 'values': '{}|{}'.format(attr_filename, hash["ssdeep_hash"]), 'comment': comment, 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': include_static_to_ids}) if pattern == "ips": for el in patterns[pattern]: values = el["ip_address"] @@ -374,7 +374,7 @@ def vmrayArtifacts(patterns): return y else: - return false + return False def vmrayCleanup(x): From 9af06fd24c116d4733498a9b0332ecaff88cb63c Mon Sep 17 00:00:00 2001 From: Steve Clement Date: Thu, 2 May 2019 11:23:49 +0900 Subject: [PATCH 115/207] fix: [pep8] More fixes --- .../modules/import_mod/vmray_import.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/misp_modules/modules/import_mod/vmray_import.py b/misp_modules/modules/import_mod/vmray_import.py index 068c820..8b79838 100644 --- a/misp_modules/modules/import_mod/vmray_import.py +++ b/misp_modules/modules/import_mod/vmray_import.py @@ -26,32 +26,32 @@ moduleinfo = {'version': '0.2', 'author': 'Koen Van Impe', 'module-type': ['import']} userConfig = {'include_analysisid': {'type': 'Boolean', 'message': 'Include link to VMRay analysis' - }, + }, 'include_analysisdetails': {'type': 'Boolean', 'message': 'Include (textual) analysis details' - }, + }, 'include_vtidetails': {'type': 'Boolean', 'message': 'Include VMRay Threat Identifier (VTI) rules' - }, - 'include_imphash_ssdeep': {'type': 'Boolean', - 'message': 'Include imphash and ssdeep' }, + 'include_imphash_ssdeep': {'type': 'Boolean', + 'message': 'Include imphash and ssdeep' + }, 'include_extracted_files': {'type': 'Boolean', 'message': 'Include extracted files section' - }, + }, 'sample_id': {'type': 'Integer', 'errorMessage': 'Expected a sample ID', 'message': 'The VMRay sample_id' } - } + } moduleconfig = ['apikey', 'url', 'wait_period'] + def handler(q=False): global include_analysisid, include_imphash_ssdeep, include_extracted_files, include_analysisdetails, include_vtidetails, include_static_to_ids - if q is False: return False request = json.loads(q) @@ -72,6 +72,7 @@ def handler(q=False): return misperrors if sample_id > 0: + e = None try: api = VMRayRESTAPI(request["config"].get("url"), request["config"].get("apikey"), False) vmray_results = {'results': []} @@ -179,10 +180,10 @@ def vmrayDownloadAnalysis(api, analysis_id): else: return False + def vmrayVti(vti): '''VMRay Threat Identifier (VTI) rules that matched for this analysis''' - if vti: r = {'results': []} for rule in vti: From 559ed786ba95c743a87582c233fe8e4d77d5ece3 Mon Sep 17 00:00:00 2001 From: Steve Clement Date: Thu, 2 May 2019 11:44:32 +0900 Subject: [PATCH 116/207] chg: [pep8] try/except # noqa Not sure how to make flake happy on this one. --- misp_modules/modules/import_mod/vmray_import.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/misp_modules/modules/import_mod/vmray_import.py b/misp_modules/modules/import_mod/vmray_import.py index 8b79838..936ab98 100644 --- a/misp_modules/modules/import_mod/vmray_import.py +++ b/misp_modules/modules/import_mod/vmray_import.py @@ -72,7 +72,6 @@ def handler(q=False): return misperrors if sample_id > 0: - e = None try: api = VMRayRESTAPI(request["config"].get("url"), request["config"].get("apikey"), False) vmray_results = {'results': []} @@ -130,7 +129,7 @@ def handler(q=False): else: misperrors['error'] = "Unable to fetch sample id %u" % (sample_id) return misperrors - except Exception as e: + except Exception as e: # noqa misperrors['error'] = "Unable to access VMRay API : %s" % (e) return misperrors else: @@ -174,7 +173,7 @@ def vmrayDownloadAnalysis(api, analysis_id): try: data = api.call("GET", "/rest/analysis/%u/archive/logs/summary.json" % (analysis_id), raw_data=True) return json.loads(data.read().decode()) - except Exception as e: + except Exception as e: # noqa misperrors['error'] = "Unable to download summary.json for analysis %s" % (analysis_id) return misperrors else: From 6f4b88606b0d30b8f85e7a3a52d77599ab21eea1 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Thu, 2 May 2019 14:07:36 +0200 Subject: [PATCH 117/207] fix: Make pep8 happy --- misp_modules/modules/import_mod/csvimport.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/misp_modules/modules/import_mod/csvimport.py b/misp_modules/modules/import_mod/csvimport.py index ec55a10..8e8bf1c 100644 --- a/misp_modules/modules/import_mod/csvimport.py +++ b/misp_modules/modules/import_mod/csvimport.py @@ -26,8 +26,8 @@ duplicatedFields = {'mispType': {'mispComment': 'comment'}, attributesFields = ['type', 'value', 'category', 'to_ids', 'comment', 'distribution'] misp_standard_csv_header = ['uuid', 'event_id', 'category', 'type', 'value', 'comment', 'to_ids', 'date', 'object_relation', 'attribute_tag', 'object_uuid', 'object_name', 'object_meta_category'] -misp_context_additional_fields = ['event_info','event_member_org','event_source_org','event_distribution', - 'event_threat_level_id','event_analysis','event_date','event_tag'] +misp_context_additional_fields = ['event_info', 'event_member_org', 'event_source_org', 'event_distribution', + 'event_threat_level_id', 'event_analysis', 'event_date', 'event_tag'] misp_extended_csv_header = misp_standard_csv_header + misp_context_additional_fields delimiters = [',', ';', '|', '/', '\t', ' '] @@ -84,7 +84,8 @@ class CsvParser(): return_data.append(line) # find which delimiter is used self.delimiter = self.find_delimiter() - if self.fields_number == 0: self.header = return_data[0].split(self.delimiter) + if self.fields_number == 0: + self.header = return_data[0].split(self.delimiter) self.data = return_data[1:] if self.has_header else return_data def parse_delimiter(self, line): @@ -112,10 +113,11 @@ class CsvParser(): attribute = {} try: try: - a_uuid,_,a_category,a_type,value,comment,to_ids,_,relation,o_uuid,o_name,o_category = line[:header_length] + a_uuid, _, a_category, a_type, value, comment, to_ids, _, relation, o_uuid, o_name, o_category = line[:header_length] except ValueError: - a_uuid,_,a_category,a_type,value,comment,to_ids,_,relation,tag,o_uuid,o_name,o_category = line[:header_length] - if tag: attribute['tags'] = tag + a_uuid, _, a_category, a_type, value, comment, to_ids, _, relation, tag, o_uuid, o_name, o_category = line[:header_length] + if tag: + attribute['tags'] = tag except ValueError: continue for t, v in zip(attribute_fields, [a_uuid, a_category, a_type, value, comment]): @@ -123,13 +125,13 @@ class CsvParser(): attribute['to_ids'] = True if to_ids == '1' else False if relation: attribute["object_relation"] = relation.replace('"', '') - object_index = tuple(o.replace('"', '') for o in (o_uuid,o_name,o_category)) + object_index = tuple(o.replace('"', '') for o in (o_uuid, o_name, o_category)) objects[object_index].append(attribute) else: l_attributes.append(attribute) for keys, attributes in objects.items(): misp_object = {} - for t, v in zip(['uuid','name','meta-category'], keys): + for t, v in zip(['uuid', 'name', 'meta-category'], keys): misp_object[t] = v misp_object['Attribute'] = attributes l_objects.append(misp_object) From d4bc85259db1a95c4d794cd804346a4174b15662 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Thu, 2 May 2019 14:15:12 +0200 Subject: [PATCH 118/207] fix: Removed unused library --- misp_modules/modules/expansion/urlhaus.py | 1 - 1 file changed, 1 deletion(-) diff --git a/misp_modules/modules/expansion/urlhaus.py b/misp_modules/modules/expansion/urlhaus.py index dae6fd6..12893b9 100644 --- a/misp_modules/modules/expansion/urlhaus.py +++ b/misp_modules/modules/expansion/urlhaus.py @@ -1,4 +1,3 @@ -from collections import defaultdict from pymisp import MISPAttribute, MISPEvent, MISPObject import json import requests From 1cd60790fde47df29c0111b3db4e2fa42e40f6cf Mon Sep 17 00:00:00 2001 From: Koen Van Impe Date: Mon, 6 May 2019 16:36:26 +0200 Subject: [PATCH 119/207] Bugfix for "sources" ; do not include as IDS for "access" registry keys - Bugfix to query "operations" in files, mutex, registry - Do not set IDS flag for registry 'access' operations --- .../modules/import_mod/vmray_import.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/misp_modules/modules/import_mod/vmray_import.py b/misp_modules/modules/import_mod/vmray_import.py index 936ab98..824c970 100644 --- a/misp_modules/modules/import_mod/vmray_import.py +++ b/misp_modules/modules/import_mod/vmray_import.py @@ -127,9 +127,14 @@ def handler(q=False): misperrors['error'] = "No vti_results returned or jobs not finished" return misperrors else: + if "result" in data: + if data["result"] == "ok": + return vmray_results + + # Fallback misperrors['error'] = "Unable to fetch sample id %u" % (sample_id) return misperrors - except Exception as e: # noqa + except Exception as e: # noqa misperrors['error'] = "Unable to access VMRay API : %s" % (e) return misperrors else: @@ -173,7 +178,7 @@ def vmrayDownloadAnalysis(api, analysis_id): try: data = api.call("GET", "/rest/analysis/%u/archive/logs/summary.json" % (analysis_id), raw_data=True) return json.loads(data.read().decode()) - except Exception as e: # noqa + except Exception as e: # noqa misperrors['error'] = "Unable to download summary.json for analysis %s" % (analysis_id) return misperrors else: @@ -337,7 +342,7 @@ def vmrayArtifacts(patterns): for el in patterns[pattern]: values = el["mutex_name"] types = ["mutex"] - if "sources" in el: + if "operations" in el: sources = el["operations"] comment = "Operations: " + ", ".join(str(x) for x in sources) else: @@ -348,18 +353,21 @@ def vmrayArtifacts(patterns): for el in patterns[pattern]: values = el["reg_key_name"] types = ["regkey"] - if "sources" in el: + include_static_to_ids_tmp = include_static_to_ids + if "operations" in el: sources = el["operations"] + if sources == ["access"]: + include_static_to_ids_tmp = False comment = "Operations: " + ", ".join(str(x) for x in sources) else: comment = "" - r['results'].append({'types': types, 'values': values, 'comment': comment, 'to_ids': include_static_to_ids}) + r['results'].append({'types': types, 'values': values, 'comment': comment, 'to_ids': include_static_to_ids_tmp}) if pattern == "urls": for el in patterns[pattern]: values = el["url"] types = ["url"] - if "sources" in el: + if "operations" in el: sources = el["operations"] comment = "Operations: " + ", ".join(str(x) for x in sources) else: From ae5bd8d06a07ddc67b581ca8e1483f4068a6333c Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 6 May 2019 22:15:14 +0200 Subject: [PATCH 120/207] fix: Clearer user config messages displayed in the import view --- misp_modules/modules/import_mod/csvimport.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/misp_modules/modules/import_mod/csvimport.py b/misp_modules/modules/import_mod/csvimport.py index 8e8bf1c..ebd09d7 100644 --- a/misp_modules/modules/import_mod/csvimport.py +++ b/misp_modules/modules/import_mod/csvimport.py @@ -14,10 +14,10 @@ moduleinfo = {'version': '0.1', 'author': 'Christian Studer', moduleconfig = [] userConfig = {'header': { 'type': 'String', - 'message': 'Define the header of the csv file, with types (included in MISP attribute types or attribute fields) separated by commas.\nFor fields that do not match these types, please use space or simply nothing between commas.\nFor instance: ip-src,domain, ,timestamp'}, + 'message': 'Define the header of the csv file, with types (included in MISP attribute types or attribute fields) separated by commas.\nFor fields that do not match these types or that you want to skip, please use space or simply nothing between commas.\nFor instance: ip-src,domain, ,timestamp'}, 'has_header': { 'type': 'Boolean', - 'message': 'Tick this box ONLY if there is a header line, NOT COMMENTED, in the file (which will be skipped atm).' + 'message': 'Tick this box ONLY if there is a header line, NOT COMMENTED, and all the fields of this header are respecting the recommendations above.' }} mispattributes = {'userConfig': userConfig, 'inputSource': ['file'], 'format': 'misp_standard'} From 28eb92da53e9c53a3fe4b9fa0c2fe9ad02a37377 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 6 May 2019 22:16:14 +0200 Subject: [PATCH 121/207] fix: Using pymisp classes & methods to parse the module results --- misp_modules/modules/import_mod/csvimport.py | 58 ++++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/misp_modules/modules/import_mod/csvimport.py b/misp_modules/modules/import_mod/csvimport.py index ebd09d7..6701d3d 100644 --- a/misp_modules/modules/import_mod/csvimport.py +++ b/misp_modules/modules/import_mod/csvimport.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- from collections import defaultdict +from pymisp import MISPEvent, MISPObject +from pymisp import __path__ as pymisp_path import csv import io import json @@ -35,6 +37,7 @@ delimiters = [',', ';', '|', '/', '\t', ' '] class CsvParser(): def __init__(self, header, has_header, data): data_header = data[0] + self.misp_event = MISPEvent() if data_header == misp_standard_csv_header or data_header == misp_extended_csv_header: self.header = misp_standard_csv_header if data_header == misp_standard_csv_header else misp_extended_csv_header[:13] self.from_misp = True @@ -50,7 +53,6 @@ class CsvParser(): self.has_delimiter = True self.fields_number, self.delimiter, self.header = self.get_delimiter_from_header(data[0]) self.data = data - self.result = [] def get_delimiter_from_header(self, data): delimiters_count = {} @@ -104,38 +106,30 @@ class CsvParser(): self.buildAttributes() def build_misp_event(self): - l_attributes = [] - l_objects = [] - objects = defaultdict(list) + objects = {} header_length = len(self.header) - attribute_fields = self.header[:1] + self.header[2:6] + attribute_fields = self.header[:1] + self.header[2:6] + self.header[7:8] for line in self.data: attribute = {} try: - try: - a_uuid, _, a_category, a_type, value, comment, to_ids, _, relation, o_uuid, o_name, o_category = line[:header_length] - except ValueError: - a_uuid, _, a_category, a_type, value, comment, to_ids, _, relation, tag, o_uuid, o_name, o_category = line[:header_length] - if tag: - attribute['tags'] = tag + a_uuid, _, a_category, a_type, value, comment, to_ids, timestamp, relation, tag, o_uuid, o_name, o_category = line[:header_length] except ValueError: continue - for t, v in zip(attribute_fields, [a_uuid, a_category, a_type, value, comment]): - attribute[t] = v.replace('"', '') + for t, v in zip(attribute_fields, (a_uuid, a_category, a_type, value, comment, timestamp)): + attribute[t] = v.strip('"') attribute['to_ids'] = True if to_ids == '1' else False + if tag: + attribute['Tag'] = [{'name': t.strip()} for t in tag.split(',')] if relation: - attribute["object_relation"] = relation.replace('"', '') - object_index = tuple(o.replace('"', '') for o in (o_uuid, o_name, o_category)) - objects[object_index].append(attribute) + if o_uuid not in objects: + objects[o_uuid] = MISPObject(o_name) + objects[o_uuid].add_attribute(relation, **attribute) else: - l_attributes.append(attribute) - for keys, attributes in objects.items(): - misp_object = {} - for t, v in zip(['uuid', 'name', 'meta-category'], keys): - misp_object[t] = v - misp_object['Attribute'] = attributes - l_objects.append(misp_object) - self.result = {"Attribute": l_attributes, "Object": l_objects} + self.misp_event.add_attribute(**attribute) + for uuid, misp_object in objects.items(): + misp_object.uuid = uuid + self.misp_event.add_object(**misp_object) + self.finalize_results() def buildAttributes(self): # if there is only 1 field of data @@ -144,7 +138,7 @@ class CsvParser(): for data in self.data: d = data.strip() if d: - self.result.append({'types': mispType, 'values': d}) + self.misp_event.add_attribute(**{'type': mispType, 'value': d}) else: # split fields that should be recognized as misp attribute types from the others list2pop, misp, head = self.findMispTypes() @@ -160,14 +154,15 @@ class CsvParser(): datamisp.append(datasplit.pop(l).strip()) # for each misp type, we create an attribute for m, dm in zip(misp, datamisp): - attribute = {'types': m, 'values': dm} + attribute = {'type': m, 'value': dm} for h, ds in zip(head, datasplit): if h: attribute[h] = ds.strip() - self.result.append(attribute) + self.misp_event.add_attribute(**attribute) + self.finalize_results() def findMispTypes(self): - descFilename = os.path.join(pymisp.__path__[0], 'data/describeTypes.json') + descFilename = os.path.join(pymisp_path[0], 'data/describeTypes.json') with open(descFilename, 'r') as f: MispTypes = json.loads(f.read())['result'].get('types') list2pop = [] @@ -196,6 +191,10 @@ class CsvParser(): # return list of indexes of the misp types, list of the misp types, remaining fields that will be attribute fields return list2pop, misp, list(reversed(head)) + def finalize_results(self): + event=json.loads(self.misp_event.to_json())['Event'] + self.results = {key: event[key] for key in ('Attribute', 'Object') if (key in event and event[key])} + def handler(q=False): if q is False: @@ -221,8 +220,7 @@ def handler(q=False): csv_parser = CsvParser(header, has_header, data) # build the attributes csv_parser.parse_csv() - r = {'results': csv_parser.result} - return r + return {'results': csv_parser.results} def introspection(): From f1b5f05bb33be21f2322dfdcbccf141b0a18b81b Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Tue, 7 May 2019 09:35:56 +0200 Subject: [PATCH 122/207] fix: Checking not MISP header fields - Rejecting fields not recognizable by MISP --- misp_modules/modules/import_mod/csvimport.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/misp_modules/modules/import_mod/csvimport.py b/misp_modules/modules/import_mod/csvimport.py index 6701d3d..fd2f27b 100644 --- a/misp_modules/modules/import_mod/csvimport.py +++ b/misp_modules/modules/import_mod/csvimport.py @@ -53,6 +53,13 @@ class CsvParser(): self.has_delimiter = True self.fields_number, self.delimiter, self.header = self.get_delimiter_from_header(data[0]) self.data = data + descFilename = os.path.join(pymisp_path[0], 'data/describeTypes.json') + with open(descFilename, 'r') as f: + self.MispTypes = json.loads(f.read())['result'].get('types') + for h in self.header: + if not (h in self.MispTypes or h in misp_extended_csv_header): + misperrors['error'] = 'Wrong header field: {}. Please use a header value that can be recognized by MISP (or alternatively skip it using a whitespace).'.format(h) + return misperrors def get_delimiter_from_header(self, data): delimiters_count = {} @@ -162,16 +169,13 @@ class CsvParser(): self.finalize_results() def findMispTypes(self): - descFilename = os.path.join(pymisp_path[0], 'data/describeTypes.json') - with open(descFilename, 'r') as f: - MispTypes = json.loads(f.read())['result'].get('types') list2pop = [] misp = [] head = [] for h in reversed(self.header): n = self.header.index(h) # fields that are misp attribute types - if h in MispTypes: + if h in self.MispTypes: list2pop.append(n) misp.append(h) # handle confusions between misp attribute types and attribute fields From 77db21cf181034a7569b60a85c13771e1773cad0 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Tue, 7 May 2019 09:37:21 +0200 Subject: [PATCH 123/207] fix: Making pep8 happy --- misp_modules/modules/import_mod/csvimport.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/misp_modules/modules/import_mod/csvimport.py b/misp_modules/modules/import_mod/csvimport.py index fd2f27b..5d7408c 100644 --- a/misp_modules/modules/import_mod/csvimport.py +++ b/misp_modules/modules/import_mod/csvimport.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from collections import defaultdict from pymisp import MISPEvent, MISPObject from pymisp import __path__ as pymisp_path import csv @@ -7,7 +6,6 @@ import io import json import os import base64 -import pymisp misperrors = {'error': 'Error'} moduleinfo = {'version': '0.1', 'author': 'Christian Studer', @@ -196,7 +194,7 @@ class CsvParser(): return list2pop, misp, list(reversed(head)) def finalize_results(self): - event=json.loads(self.misp_event.to_json())['Event'] + event = json.loads(self.misp_event.to_json())['Event'] self.results = {key: event[key] for key in ('Attribute', 'Object') if (key in event and event[key])} From d66f7932f78046bc865031b5b8ab71254d2b09ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Tue, 7 May 2019 11:24:03 +0200 Subject: [PATCH 124/207] chg: Bump dependencies --- Pipfile.lock | 95 ++++++++++++++++++++++++++-------------------------- 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index fa07b4c..9f395cf 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -455,7 +455,7 @@ "pybgpranking": { "editable": true, "git": "https://github.com/D4-project/BGP-Ranking.git/", - "ref": "4e0741056bcc0077de1120b8724a31330b26033e", + "ref": "aa0d4581a836503d2298ee046bea49c501eefdd1", "subdirectory": "client" }, "pydnstrails": { @@ -486,13 +486,13 @@ "pyipasnhistory": { "editable": true, "git": "https://github.com/D4-project/IPASN-History.git/", - "ref": "c0c2bbf8d70811982dad065ea463a7e01593a38d", + "ref": "47cd0f2658ab172fce42126ff3a1dbcddfb0b5fb", "subdirectory": "client" }, "pymisp": { "editable": true, "git": "https://github.com/MISP/PyMISP.git", - "ref": "582dda0ce2a8ca8e1dd2cf3842e0491caca51c62" + "ref": "e8bba395bc67bf56e41ddd022ebae670c5b0d64b" }, "pyonyphe": { "editable": true, @@ -508,10 +508,11 @@ }, "pypdns": { "hashes": [ - "sha256:0356360156dd26d2cf27a415a10ff2bd1ff1d2eb3b2dd51b35553d60b87fd328" + "sha256:349ab1033e34a60fa0c4626b3432f5202c174656955fdf330986380c9a97cf3e", + "sha256:c609678d47255a240c1e3f29a757355f610a8394ec22f21a07853360ebee6f20" ], "index": "pypi", - "version": "==1.3" + "version": "==1.4.1" }, "pypssl": { "hashes": [ @@ -549,10 +550,10 @@ }, "python-pptx": { "hashes": [ - "sha256:1f2d5d1d923d91f50a1f0ed794935e7d670993fdcb6c12c81cc83977c1f23e14" + "sha256:a857d69e52d7e8a8fb32fca8182fdd4a3c68c689de8d4e4460e9b4a95efa7bc4" ], "index": "pypi", - "version": "==0.6.17" + "version": "==0.6.18" }, "pytz": { "hashes": [ @@ -602,37 +603,37 @@ }, "reportlab": { "hashes": [ - "sha256:13714baa9753bfca94df67716cccb3eedcaaa30cf7bc40b282d338a718e0b610", - "sha256:16c1bb717a1a0e2ed065aa31eb5968dc03b34b728926216ef282cefeebf50c1b", - "sha256:2d9d66770880e8d112b6b925458593d34b84947c355847578cd974df0a3e3b8b", - "sha256:3334a30e477e1dfa0276eb41ed5bfd2a684c9917e55c6acb30d91abac46555f6", - "sha256:33796ea88d20c05958903c11ff34d896e462381f4a0f550854aabe6dd07cc189", - "sha256:5184f53c0babeedb4ebe297eb97794822cb122456ca03411c68256730c998d48", - "sha256:53589c5db35041920cd7a92a171506ff4eb5542ab8415af272fe4558927399a8", - "sha256:58ba0a9ca51d666d55ec7ecd83ab14763b79e7e5e0775b7717694e94c2fbbf18", - "sha256:6998652beba357da9687eba13b46ceccd0a7a2153d656cf8a03b7494c915e077", - "sha256:6c3b07c8a94ee9609c31a51e4131891c8330ffd379db23ab582fd225a06a4e59", - "sha256:7b248d2d9d4ab6d4cad91eb2b153b2c4c7b3fced89cb5a5b5bfbc7d09593871a", - "sha256:81d991c9994a576ea053b281b8c9afc28b12261197d478e72055d381f60fa26f", - "sha256:8a9a8be6841b88b13aa9c0f7d193c6d24b04b10c2e7cbf6657b1807bac5b1e9f", - "sha256:8de3107436e68014890adcec446207fd98d60c26e7feae6f550eea9eab3a622d", - "sha256:90f85afb68f7cd4fd8681af3123d23488274e4d1c3fea8d3831ef7257d9733c8", - "sha256:94857052c951ffa56de95cfce483fdf3de19587db4b1bc4f6a4043fb1c4af772", - "sha256:a47603d9b975e8870ed30ade22db3478b030dd7a6041c8272c3719d9bbeaef34", - "sha256:a5671b152d3f85963d8450e956ddecfb5d30af62dd1f73207fab9aa32a0240d2", - "sha256:a745cd1a4368fac093deff3b65614f052eced6afa9ed7fe223da2a52813f2e23", - "sha256:af454e8e844e3eeace5aead02701748b2a908f3e8cbc386cc5ddc185cef8c57f", - "sha256:c3c6c1234eed451111e969c776688e866554cb362250b88f782ab80ea62f9114", - "sha256:cc1cf8ba1b2f1669f5d873a7cfdb9e07a920243d74a66a78f8afa2cf78587864", - "sha256:cce3c9b0e115ea5553615a647b6644e5724bdc776e778593ffa5f383d907afb2", - "sha256:d137feacef83627d10adb869aa6998f29eb7af4cff3101c9fc94a1d73943b6cc", - "sha256:d7213814d050ca3b0bf7e018f94ed947e90477cd36aff298ff5932b849a0f36a", - "sha256:e381d08675807d2bb465717f69818040173351650af82730d721ecad429279a6", - "sha256:e39c6efdd64027be56ce991f7ffb86c7cee47da8c844c3544bbd68ef842831a0", - "sha256:f8526cfbbad599d22de6eb59b5a43610ba9b28f74ac2406125fe803f31a262a6" + "sha256:04b9bf35127974f734bddddf48860732361e31c1220c0ebe4f683f19d5cfc3b8", + "sha256:073da867efdf9e0d6cba2a566f5929ef0bb9fb757b53a7132b91db9869441859", + "sha256:08e6e63a4502d3a00062ba9ff9669f95577fbdb1a5f8c6cdb1230c5ee295273a", + "sha256:0960567b9d937a288efa04753536dce1dbb032a1e1f622fd92efbe85b8cccf6e", + "sha256:1870e321c5d7772fd6e5538a89562ed8b40687ed0aec254197dc73e9d700e62f", + "sha256:1eac902958a7f66c30e1115fa1a80bf6a7aa57680427cfcb930e13c746142150", + "sha256:1f6cdcdaf6ab78ab3efd21b23c27e4487a5c0816202c3578b277f441f984a51f", + "sha256:281443252a335489ce4b8b150afccdc01c74daf97e962fd99a8c2d59c8b333d3", + "sha256:2ae66e61b03944c5ed1f3c96bbc51160cce4aa28cbe96f205b464017cdfc851c", + "sha256:34d348575686390676757876fef50f6e32e3a59ff7d549e022b5f3b8a9f7e564", + "sha256:508224a11ec9ef203ae2fd2177e903d36d3b840eeb8ac70747f53eeb373db439", + "sha256:5c497c9597a346d27007507cddc2a792f8ca5017268738fd35c374c224d81988", + "sha256:6e0d9efe78526ddf5ad1d2357f6b2b0f5d7df354ac559358e3d056bdd12fdabf", + "sha256:817dfd400c5e694cbb6eb87bc932cd3d97cf5d79d918329b8f99085a7979bb29", + "sha256:8d6ed4357eb0146501ebdb7226c87ef98a9bcbc6d54401ec676fa905b6355e00", + "sha256:8e681324ce457cc3d5c0949c92d590ac4401347b5df55f6fde207b42316d42d2", + "sha256:926981544d37554b44c6f067c3f94981831f9ef3f2665fa5f4114b23a140f596", + "sha256:92a0bf5cc2d9418115bff46032964d25bb21c0ac8bcdf6bee5769ca810a54a5a", + "sha256:9a3e7495e223fc4a9bdcd356972c230d32bf8c7a57442ca5b8c2ff6b19e6007b", + "sha256:a31f424020176e96a0ff0229f7f251d865c5409ddf074f695b97ba604f173b48", + "sha256:aa0c35b22929c19ecd48d5c1734e420812f269f463d1ef138e0adb28069c3150", + "sha256:b36b555cdbdd51f9f00a7606966ec6d4d30d74c61d1523a1ac56bbeb83a15ed3", + "sha256:cd3d9765b8f446c25d75a4456d8781c4781de0f10f860dff5cb69bbe526e8f53", + "sha256:d3daa4f19d1dc2fc1fc2591e1354edd95439b9e9953ca8b374d41524d434b315", + "sha256:d8f1878bc1fc91c63431e9b0f1940ff18b70c059f6d38f2be1e34ce9ffcc28ea", + "sha256:ddca7479d29f9dfbfc69057764239ec7753b49a3b0dcbed08f70cbef8fccfee6", + "sha256:f28f3a965d15c88c797cf33968bdaa5a04aabcf321d3f6fcf14d7e7fde8d90f3", + "sha256:fcca214bf340f59245fff792134a9ac333d21eeef19a874a69ecc926b4c992a4" ], "index": "pypi", - "version": "==3.5.20" + "version": "==3.5.21" }, "requests": { "hashes": [ @@ -651,10 +652,10 @@ }, "shodan": { "hashes": [ - "sha256:c30baebce853ad67677bf002dde96a1ca1a9729bdd300fbb3c5e5d889547a639" + "sha256:4aa8ea11448159147dbdf65b2aa3b10d47c05decd94992fdd016efdc7781e91b" ], "index": "pypi", - "version": "==1.12.1" + "version": "==1.13.0" }, "sigmatools": { "hashes": [ @@ -727,10 +728,10 @@ }, "urllib3": { "hashes": [ - "sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0", - "sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3" + "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", + "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" ], - "version": "==1.24.2" + "version": "==1.24.3" }, "uwhois": { "editable": true, @@ -766,10 +767,10 @@ }, "xlsxwriter": { "hashes": [ - "sha256:2a40b427dac0f640031e5b33abe97e761de6e0f12d4d346e7b2e2b67cf6ee927", - "sha256:431edc9ba1132eec1996939aa83fffe41885d3042ab09d47c3086f41a156c430" + "sha256:5ec6aa71f6ae4b6298376d8b6a56ca9cdcb8b80323a444212226447aed4fa10f", + "sha256:ec51d99c0cc5d95ec8d8e9c8de7c8fbbf461988bec01a8c86b5155a6716b0a5a" ], - "version": "==1.1.7" + "version": "==1.1.8" }, "yara-python": { "hashes": [ @@ -977,10 +978,10 @@ }, "urllib3": { "hashes": [ - "sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0", - "sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3" + "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", + "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" ], - "version": "==1.24.2" + "version": "==1.24.3" } } } From 728386d8a0ff0ac32856ccf48a4b37ed53542d1a Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Wed, 8 May 2019 16:52:49 +0200 Subject: [PATCH 125/207] add: [new_module] Module to import data from Joe sandbox reports - Parsing file, pe and pe-section objects from the report file info field - Deeper file info parsing to come - Other fields parsing to come as well --- misp_modules/modules/import_mod/joe_import.py | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 misp_modules/modules/import_mod/joe_import.py diff --git a/misp_modules/modules/import_mod/joe_import.py b/misp_modules/modules/import_mod/joe_import.py new file mode 100644 index 0000000..bc14ba8 --- /dev/null +++ b/misp_modules/modules/import_mod/joe_import.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +from pymisp import MISPEvent, MISPObject +import json +import base64 + +misperrors = {'error': 'Error'} +userConfig = {} +inputSource = ['file'] + +moduleinfo = {'version': '0.1', 'author': 'Christian Studer', + 'description': 'Import for Joe Sandbox JSON reports', + 'module-type': ['import']} + +moduleconfig = [] + +file_object_fields = ['filename', 'md5', 'sha1', 'sha256', 'sha512', 'ssdeep'] +file_object_mapping = {'entropy': ('float', 'entropy'), + 'filesize': ('size-in-bytes', 'size-in-bytes'), + 'filetype': ('mime-type', 'mimetype')} +pe_object_fields = {'entrypoint': ('text', 'entrypoint-address'), + 'imphash': ('imphash', 'imphash')} +pe_object_mapping = {'CompanyName': 'company-name', 'FileDescription': 'file-description', + 'FileVersion': 'file-version', 'InternalName': 'internal-filename', + 'LegalCopyright': 'legal-copyright', 'OriginalFilename': 'original-filename', + 'ProductName': 'product-filename', 'ProductVersion': 'product-version', + 'Translation': 'lang-id'} +section_object_mapping = {'characteristics': ('text', 'characteristic'), + 'entropy': ('float', 'entropy'), + 'name': ('text', 'name'), 'rawaddr': ('hex', 'offset'), + 'rawsize': ('size-in-bytes', 'size-in-bytes'), + 'virtaddr': ('hex', 'virtual_address'), + 'virtsize': ('size-in-bytes', 'virtual_size')} +signerinfo_object_mapping = {'sigissuer': ('text', 'issuer'), + 'version': ('text', 'version')} + + +class JoeParser(): + def __init__(self, data): + self.data = data + self.misp_event = MISPEvent() + + def parse_joe(self): + self.parse_fileinfo() + self.finalize_results() + + def parse_fileinfo(self): + fileinfo = self.data['fileinfo'] + file_object = MISPObject('file') + for field in file_object_fields: + file_object.add_attribute(field, **{'type': field, 'value': fileinfo[field]}) + for field, mapping in file_object_mapping.items(): + attribute_type, object_relation = mapping + file_object.add_attribute(object_relation, **{'type': attribute_type, 'value': fileinfo[field]}) + pe_object = MISPObject('pe') + file_object.add_reference(pe_object.uuid, 'included-in') + self.misp_event.add_object(**file_object) + peinfo = fileinfo['pe'] + for field, mapping in pe_object_fields.items(): + attribute_type, object_relation = mapping + pe_object.add_attribute(object_relation, **{'type': attribute_type, 'value': peinfo[field]}) + pe_object.add_attribute('compilation-timestamp', **{'type': 'datetime', 'value': int(peinfo['timestamp'].split()[0], 16)}) + program_name = fileinfo['filename'] + for feature in peinfo['versions']['version']: + name = feature['name'] + if name == 'InternalName': + program_name = feature['value'] + pe_object.add_attribute(pe_object_mapping[name], **{'type': 'text', 'value': feature['value']}) + sections_number = len(peinfo['sections']['section']) + pe_object.add_attribute('number-sections', **{'type': 'counter', 'value': sections_number}) + for section in peinfo['sections']['section']: + section_object = self.parse_pe_section(section) + pe_object.add_reference(section_object.uuid, 'included-in') + self.misp_event.add_object(**section_object) + signerinfo_object = MISPObject('authenticode-signerinfo') + pe_object.add_reference(signerinfo_object.uuid, 'signed-by') + self.misp_event.add_object(**pe_object) + signerinfo_object.add_attribute('program-name', **{'type': 'text', 'value': program_name}) + signatureinfo = peinfo['signature'] + for feature, mapping in signerinfo_object_mapping.items(): + attribute_type, object_relation = mapping + signerinfo_object.add_attribute(object_relation, **{'type': attribute_type, 'value': signatureinfo[feature]}) + self.misp_event.add_object(**signerinfo_object) + + def parse_pe_section(self, section): + section_object = MISPObject('pe-section') + for feature, mapping in section_object_mapping.items(): + attribute_type, object_relation = mapping + section_object.add_attribute(object_relation, **{'type': attribute_type, 'value': section[feature]}) + return section_object + + def finalize_results(self): + event = json.loads(self.misp_event.to_json())['Event'] + self.results = {key: event[key] for key in ('Attribute', 'Object') if (key in event and event[key])} + + +def handler(q=False): + if q is False: + return False + q = json.loads(q) + data = base64.b64decode(q.get('data')).decode('utf-8') + if not data: + return json.dumps({'success': 0}) + joe_data = json.loads(data)['analysis'] + print(type(joe_data)) + joe_parser = JoeParser(joe_data) + joe_parser.parse_joe() + return {'results': joe_parser.results} + + +def introspection(): + modulesetup = {} + try: + userConfig + modulesetup['userConfig'] = userConfig + except NameError: + pass + try: + inputSource + modulesetup['inputSource'] = inputSource + except NameError: + pass + modulesetup['format'] = 'misp_standard' + return modulesetup + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo From d39fb7da184f2335a2b860b384106c57c6a0bffc Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 13 May 2019 17:29:07 +0200 Subject: [PATCH 126/207] add: Parsing some object references at the end of the process --- misp_modules/modules/import_mod/joe_import.py | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/misp_modules/modules/import_mod/joe_import.py b/misp_modules/modules/import_mod/joe_import.py index bc14ba8..e378bc2 100644 --- a/misp_modules/modules/import_mod/joe_import.py +++ b/misp_modules/modules/import_mod/joe_import.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from collections import defaultdict from pymisp import MISPEvent, MISPObject import json import base64 @@ -38,11 +39,21 @@ class JoeParser(): def __init__(self, data): self.data = data self.misp_event = MISPEvent() + self.references = defaultdict(list) def parse_joe(self): self.parse_fileinfo() + if self.references: + self.build_references() self.finalize_results() + def build_references(self): + for misp_object in self.misp_event.objects: + object_uuid = misp_object.uuid + if object_uuid in self.references: + for reference in self.references[object_uuid]: + misp_object.add_reference(reference['idref'], reference['relationship']) + def parse_fileinfo(self): fileinfo = self.data['fileinfo'] file_object = MISPObject('file') @@ -54,6 +65,7 @@ class JoeParser(): pe_object = MISPObject('pe') file_object.add_reference(pe_object.uuid, 'included-in') self.misp_event.add_object(**file_object) + self.fileinfo_uuid = file_object.uuid peinfo = fileinfo['pe'] for field, mapping in pe_object_fields.items(): attribute_type, object_relation = mapping @@ -67,10 +79,6 @@ class JoeParser(): pe_object.add_attribute(pe_object_mapping[name], **{'type': 'text', 'value': feature['value']}) sections_number = len(peinfo['sections']['section']) pe_object.add_attribute('number-sections', **{'type': 'counter', 'value': sections_number}) - for section in peinfo['sections']['section']: - section_object = self.parse_pe_section(section) - pe_object.add_reference(section_object.uuid, 'included-in') - self.misp_event.add_object(**section_object) signerinfo_object = MISPObject('authenticode-signerinfo') pe_object.add_reference(signerinfo_object.uuid, 'signed-by') self.misp_event.add_object(**pe_object) @@ -80,6 +88,10 @@ class JoeParser(): attribute_type, object_relation = mapping signerinfo_object.add_attribute(object_relation, **{'type': attribute_type, 'value': signatureinfo[feature]}) self.misp_event.add_object(**signerinfo_object) + for section in peinfo['sections']['section']: + section_object = self.parse_pe_section(section) + self.references[pe_object.uuid].append({'idref': section_object.uuid, 'relationship': 'included-in'}) + self.misp_event.add_object(**section_object) def parse_pe_section(self, section): section_object = MISPObject('pe-section') From 29e681ef81ceea22ec3b91936fb89957ec4e5c83 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 13 May 2019 17:30:01 +0200 Subject: [PATCH 127/207] add: Parsing processes called by the file analyzed in the joe sandbox report --- misp_modules/modules/import_mod/joe_import.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/misp_modules/modules/import_mod/joe_import.py b/misp_modules/modules/import_mod/joe_import.py index e378bc2..bae920f 100644 --- a/misp_modules/modules/import_mod/joe_import.py +++ b/misp_modules/modules/import_mod/joe_import.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from collections import defaultdict +from datetime import datetime from pymisp import MISPEvent, MISPObject import json import base64 @@ -25,6 +26,9 @@ pe_object_mapping = {'CompanyName': 'company-name', 'FileDescription': 'file-des 'LegalCopyright': 'legal-copyright', 'OriginalFilename': 'original-filename', 'ProductName': 'product-filename', 'ProductVersion': 'product-version', 'Translation': 'lang-id'} +process_object_fields = {'cmdline': 'command-line', 'name': 'name', + 'parentpid': 'parent-pid', 'pid': 'pid', + 'path': 'current-directory'} section_object_mapping = {'characteristics': ('text', 'characteristic'), 'entropy': ('float', 'entropy'), 'name': ('text', 'name'), 'rawaddr': ('hex', 'offset'), @@ -43,6 +47,7 @@ class JoeParser(): def parse_joe(self): self.parse_fileinfo() + self.parse_behavior() if self.references: self.build_references() self.finalize_results() @@ -54,6 +59,24 @@ class JoeParser(): for reference in self.references[object_uuid]: misp_object.add_reference(reference['idref'], reference['relationship']) + def parse_behavior(self): + self.parse_behavior_system() + self.parse_behavior_network() + + def parse_behavior_network(self): + network = self.data['behavior']['network'] + + def parse_behavior_system(self): + processes = self.data['behavior']['system']['processes']['process'][0] + general = processes['general'] + process_object = MISPObject('process') + for feature, relation in process_object_fields.items(): + process_object.add_attribute(relation, **{'type': 'text', 'value': general[feature]}) + start_time = datetime.strptime('{} {}'.format(general['date'], general['time']), '%d/%m/%Y %H:%M:%S') + process_object.add_attribute('start-time', **{'type': 'datetime', 'value': start_time}) + self.misp_event.add_object(**process_object) + self.references[self.fileinfo_uuid].append({'idref': process_object.uuid, 'relationship': 'calls'}) + def parse_fileinfo(self): fileinfo = self.data['fileinfo'] file_object = MISPObject('file') From df7047dff0bd0f36441965e4d3a53b70f457590e Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Tue, 14 May 2019 10:50:11 +0200 Subject: [PATCH 128/207] fix: Fixed output format to match with the recent changes on modules --- misp_modules/modules/import_mod/goamlimport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/import_mod/goamlimport.py b/misp_modules/modules/import_mod/goamlimport.py index add6470..79b4cfe 100644 --- a/misp_modules/modules/import_mod/goamlimport.py +++ b/misp_modules/modules/import_mod/goamlimport.py @@ -165,7 +165,7 @@ def handler(q=False): misperrors['error'] = "Impossible to read XML data" return misperrors aml_parser.parse_xml() - r = {'results': [obj.to_json() for obj in aml_parser.misp_event.objects]} + r = {'results': {'Object': [obj.to_json() for obj in aml_parser.misp_event.objects]}} return r From fc8a56d1d9874a85afc9ee75b2835617c1d601fa Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Wed, 15 May 2019 15:49:29 +0200 Subject: [PATCH 129/207] fix: Removed test print --- misp_modules/modules/import_mod/joe_import.py | 1 - 1 file changed, 1 deletion(-) diff --git a/misp_modules/modules/import_mod/joe_import.py b/misp_modules/modules/import_mod/joe_import.py index bae920f..206f108 100644 --- a/misp_modules/modules/import_mod/joe_import.py +++ b/misp_modules/modules/import_mod/joe_import.py @@ -136,7 +136,6 @@ def handler(q=False): if not data: return json.dumps({'success': 0}) joe_data = json.loads(data)['analysis'] - print(type(joe_data)) joe_parser = JoeParser(joe_data) joe_parser.parse_joe() return {'results': joe_parser.results} From d195b554a5165cd21d2ea1fbb46b3c24ff808365 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Wed, 15 May 2019 22:05:03 +0200 Subject: [PATCH 130/207] fix: Testing if some fields exist before trying to import them - Testing for pe itself, pe versions and pe signature --- misp_modules/modules/import_mod/joe_import.py | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/misp_modules/modules/import_mod/joe_import.py b/misp_modules/modules/import_mod/joe_import.py index 206f108..9e12cfb 100644 --- a/misp_modules/modules/import_mod/joe_import.py +++ b/misp_modules/modules/import_mod/joe_import.py @@ -85,21 +85,25 @@ class JoeParser(): for field, mapping in file_object_mapping.items(): attribute_type, object_relation = mapping file_object.add_attribute(object_relation, **{'type': attribute_type, 'value': fileinfo[field]}) + self.fileinfo_uuid = file_object.uuid + if not fileinfo.get('pe'): + self.misp_event.add_object(**file_object) + return + peinfo = fileinfo['pe'] pe_object = MISPObject('pe') file_object.add_reference(pe_object.uuid, 'included-in') self.misp_event.add_object(**file_object) - self.fileinfo_uuid = file_object.uuid - peinfo = fileinfo['pe'] for field, mapping in pe_object_fields.items(): attribute_type, object_relation = mapping pe_object.add_attribute(object_relation, **{'type': attribute_type, 'value': peinfo[field]}) pe_object.add_attribute('compilation-timestamp', **{'type': 'datetime', 'value': int(peinfo['timestamp'].split()[0], 16)}) program_name = fileinfo['filename'] - for feature in peinfo['versions']['version']: - name = feature['name'] - if name == 'InternalName': - program_name = feature['value'] - pe_object.add_attribute(pe_object_mapping[name], **{'type': 'text', 'value': feature['value']}) + if peinfo['versions']: + for feature in peinfo['versions']['version']: + name = feature['name'] + if name == 'InternalName': + program_name = feature['value'] + pe_object.add_attribute(pe_object_mapping[name], **{'type': 'text', 'value': feature['value']}) sections_number = len(peinfo['sections']['section']) pe_object.add_attribute('number-sections', **{'type': 'counter', 'value': sections_number}) signerinfo_object = MISPObject('authenticode-signerinfo') @@ -107,9 +111,10 @@ class JoeParser(): self.misp_event.add_object(**pe_object) signerinfo_object.add_attribute('program-name', **{'type': 'text', 'value': program_name}) signatureinfo = peinfo['signature'] - for feature, mapping in signerinfo_object_mapping.items(): - attribute_type, object_relation = mapping - signerinfo_object.add_attribute(object_relation, **{'type': attribute_type, 'value': signatureinfo[feature]}) + if signatureinfo['signed']: + for feature, mapping in signerinfo_object_mapping.items(): + attribute_type, object_relation = mapping + signerinfo_object.add_attribute(object_relation, **{'type': attribute_type, 'value': signatureinfo[feature]}) self.misp_event.add_object(**signerinfo_object) for section in peinfo['sections']['section']: section_object = self.parse_pe_section(section) From 067b2292240be703de3a029ef4da5e16e0584a02 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Wed, 15 May 2019 22:06:55 +0200 Subject: [PATCH 131/207] fix: Handling case of multiple processes in behavior field - Also starting parsing file activities --- misp_modules/modules/import_mod/joe_import.py | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/misp_modules/modules/import_mod/joe_import.py b/misp_modules/modules/import_mod/joe_import.py index 9e12cfb..4ba53b4 100644 --- a/misp_modules/modules/import_mod/joe_import.py +++ b/misp_modules/modules/import_mod/joe_import.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from collections import defaultdict from datetime import datetime -from pymisp import MISPEvent, MISPObject +from pymisp import MISPAttribute, MISPEvent, MISPObject import json import base64 @@ -29,6 +29,8 @@ pe_object_mapping = {'CompanyName': 'company-name', 'FileDescription': 'file-des process_object_fields = {'cmdline': 'command-line', 'name': 'name', 'parentpid': 'parent-pid', 'pid': 'pid', 'path': 'current-directory'} +process_references_mapping = {'fileCreated': 'creates', 'fileDeleted': 'deletes', + 'fileMoved': 'moves', 'fileRead': 'reads', 'fileWritten': 'writes'} section_object_mapping = {'characteristics': ('text', 'characteristic'), 'entropy': ('float', 'entropy'), 'name': ('text', 'name'), 'rawaddr': ('hex', 'offset'), @@ -67,15 +69,22 @@ class JoeParser(): network = self.data['behavior']['network'] def parse_behavior_system(self): - processes = self.data['behavior']['system']['processes']['process'][0] - general = processes['general'] - process_object = MISPObject('process') - for feature, relation in process_object_fields.items(): - process_object.add_attribute(relation, **{'type': 'text', 'value': general[feature]}) - start_time = datetime.strptime('{} {}'.format(general['date'], general['time']), '%d/%m/%Y %H:%M:%S') - process_object.add_attribute('start-time', **{'type': 'datetime', 'value': start_time}) - self.misp_event.add_object(**process_object) - self.references[self.fileinfo_uuid].append({'idref': process_object.uuid, 'relationship': 'calls'}) + for process in self.data['behavior']['system']['processes']['process']: + general = process['general'] + process_object = MISPObject('process') + for feature, relation in process_object_fields.items(): + process_object.add_attribute(relation, **{'type': 'text', 'value': general[feature]}) + start_time = datetime.strptime('{} {}'.format(general['date'], general['time']), '%d/%m/%Y %H:%M:%S') + process_object.add_attribute('start-time', **{'type': 'datetime', 'value': start_time}) + for feature, files in process['fileactivities'].items(): + if files: + for call in files['call']: + file_attribute = MISPAttribute() + file_attribute.from_dict(**{'type': 'filename', 'value': call['path']}) + process_object.add_reference(file_attribute.uuid, process_references_mapping[feature]) + self.misp_event.add_attribute(**file_attribute) + self.misp_event.add_object(**process_object) + self.references[self.fileinfo_uuid].append({'idref': process_object.uuid, 'relationship': 'calls'}) def parse_fileinfo(self): fileinfo = self.data['fileinfo'] From 2246fc0d02b22ad3662b444a7743fd23f4e92584 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Thu, 16 May 2019 16:11:43 +0200 Subject: [PATCH 132/207] add: Parsing registry activities under processes --- misp_modules/modules/import_mod/joe_import.py | 63 ++++++++++++++----- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/misp_modules/modules/import_mod/joe_import.py b/misp_modules/modules/import_mod/joe_import.py index 4ba53b4..8c0c514 100644 --- a/misp_modules/modules/import_mod/joe_import.py +++ b/misp_modules/modules/import_mod/joe_import.py @@ -37,6 +37,9 @@ section_object_mapping = {'characteristics': ('text', 'characteristic'), 'rawsize': ('size-in-bytes', 'size-in-bytes'), 'virtaddr': ('hex', 'virtual_address'), 'virtsize': ('size-in-bytes', 'virtual_size')} +registry_references_mapping = {'keyValueCreated': 'creates', 'keyValueModified': 'modifies'} +regkey_object_mapping = {'name': ('text', 'name'), 'newdata': ('text', 'data'), + 'path': ('regkey', 'key')} signerinfo_object_mapping = {'sigissuer': ('text', 'issuer'), 'version': ('text', 'version')} @@ -69,22 +72,28 @@ class JoeParser(): network = self.data['behavior']['network'] def parse_behavior_system(self): - for process in self.data['behavior']['system']['processes']['process']: - general = process['general'] - process_object = MISPObject('process') - for feature, relation in process_object_fields.items(): - process_object.add_attribute(relation, **{'type': 'text', 'value': general[feature]}) - start_time = datetime.strptime('{} {}'.format(general['date'], general['time']), '%d/%m/%Y %H:%M:%S') - process_object.add_attribute('start-time', **{'type': 'datetime', 'value': start_time}) - for feature, files in process['fileactivities'].items(): - if files: - for call in files['call']: - file_attribute = MISPAttribute() - file_attribute.from_dict(**{'type': 'filename', 'value': call['path']}) - process_object.add_reference(file_attribute.uuid, process_references_mapping[feature]) - self.misp_event.add_attribute(**file_attribute) - self.misp_event.add_object(**process_object) - self.references[self.fileinfo_uuid].append({'idref': process_object.uuid, 'relationship': 'calls'}) + system = self.data['behavior']['system'] + if system.get('processes'): + process_activities = {'fileactivities': self.parse_fileactivities, + 'registryactivities': self.parse_registryactivities} + for process in system['processes']['process']: + general = process['general'] + process_object = MISPObject('process') + for feature, relation in process_object_fields.items(): + process_object.add_attribute(relation, **{'type': 'text', 'value': general[feature]}) + start_time = datetime.strptime('{} {}'.format(general['date'], general['time']), '%d/%m/%Y %H:%M:%S') + process_object.add_attribute('start-time', **{'type': 'datetime', 'value': start_time}) + self.misp_event.add_object(**process_object) + for field, to_call in process_activities.items(): + to_call(process_object.uuid, process[field]) + self.references[self.fileinfo_uuid].append({'idref': process_object.uuid, 'relationship': 'calls'}) + + def parse_fileactivities(self, process_uuid, fileactivities): + for feature, files in fileactivities.items(): + if files: + for call in files['call']: + file_uuid = self.create_attribute(call, 'filename') + self.references[process_uuid].append({'idref': file_uuid, 'relationship': process_references_mapping[feature]}) def parse_fileinfo(self): fileinfo = self.data['fileinfo'] @@ -137,6 +146,28 @@ class JoeParser(): section_object.add_attribute(object_relation, **{'type': attribute_type, 'value': section[feature]}) return section_object + def parse_registryactivities(self, process_uuid, registryactivities): + if registryactivities['keyCreated']: + for call in registryactivities['keyCreated']['call']: + regkey_uuid = self.create_attribute(call, 'regkey') + self.references[process_uuid].append({'idref': regkey_uuid, 'relationship': 'creates'}) + for feature, relationship_type in registry_references_mapping.items(): + if registryactivities[feature]: + for call in registryactivities[feature]['call']: + registry_key = MISPObject('registry-key') + for field, mapping in regkey_object_mapping.items(): + attribute_type, object_relation = mapping + registry_key.add_attribute(object_relation, **{'type': attribute_type, 'value': call[field]}) + registry_key.add_attribute('data-type', **{'type': 'text', 'value': 'REG_{}'.format(call['type'].upper())}) + self.misp_event.add_object(**registry_key) + self.references[process_uuid].append({'idref': registry_key.uuid, 'relationship': relationship_type}) + + def create_attribute(self, field, attribute_type): + attribute = MISPAttribute() + attribute.from_dict(**{'type': attribute_type, 'value': field['path']}) + self.misp_event.add_attribute(**attribute) + return attribute.uuid + def finalize_results(self): event = json.loads(self.misp_event.to_json())['Event'] self.results = {key: event[key] for key in ('Attribute', 'Object') if (key in event and event[key])} From f9515c14d059599c6e54cbdbb6154d8adf3d14d9 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Thu, 16 May 2019 16:14:25 +0200 Subject: [PATCH 133/207] fix: Avoiding attribute & reference duplicates --- misp_modules/modules/import_mod/joe_import.py | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/misp_modules/modules/import_mod/joe_import.py b/misp_modules/modules/import_mod/joe_import.py index 8c0c514..6275c50 100644 --- a/misp_modules/modules/import_mod/joe_import.py +++ b/misp_modules/modules/import_mod/joe_import.py @@ -19,6 +19,8 @@ file_object_fields = ['filename', 'md5', 'sha1', 'sha256', 'sha512', 'ssdeep'] file_object_mapping = {'entropy': ('float', 'entropy'), 'filesize': ('size-in-bytes', 'size-in-bytes'), 'filetype': ('mime-type', 'mimetype')} +file_references_mapping = {'fileCreated': 'creates', 'fileDeleted': 'deletes', + 'fileMoved': 'moves', 'fileRead': 'reads', 'fileWritten': 'writes'} pe_object_fields = {'entrypoint': ('text', 'entrypoint-address'), 'imphash': ('imphash', 'imphash')} pe_object_mapping = {'CompanyName': 'company-name', 'FileDescription': 'file-description', @@ -29,8 +31,6 @@ pe_object_mapping = {'CompanyName': 'company-name', 'FileDescription': 'file-des process_object_fields = {'cmdline': 'command-line', 'name': 'name', 'parentpid': 'parent-pid', 'pid': 'pid', 'path': 'current-directory'} -process_references_mapping = {'fileCreated': 'creates', 'fileDeleted': 'deletes', - 'fileMoved': 'moves', 'fileRead': 'reads', 'fileWritten': 'writes'} section_object_mapping = {'characteristics': ('text', 'characteristic'), 'entropy': ('float', 'entropy'), 'name': ('text', 'name'), 'rawaddr': ('hex', 'offset'), @@ -49,10 +49,13 @@ class JoeParser(): self.data = data self.misp_event = MISPEvent() self.references = defaultdict(list) + self.attributes = defaultdict(lambda: defaultdict(set)) def parse_joe(self): self.parse_fileinfo() self.parse_behavior() + if self.attributes: + self.handle_attributes() if self.references: self.build_references() self.finalize_results() @@ -64,6 +67,14 @@ class JoeParser(): for reference in self.references[object_uuid]: misp_object.add_reference(reference['idref'], reference['relationship']) + def handle_attributes(self): + for attribute_type, attribute in self.attributes.items(): + for attribute_value, references in attribute.items(): + attribute_uuid = self.create_attribute(attribute_type, attribute_value) + for reference in references: + source_uuid, relationship = reference + self.references[source_uuid].append({'idref': attribute_uuid, 'relationship': relationship}) + def parse_behavior(self): self.parse_behavior_system() self.parse_behavior_network() @@ -92,8 +103,7 @@ class JoeParser(): for feature, files in fileactivities.items(): if files: for call in files['call']: - file_uuid = self.create_attribute(call, 'filename') - self.references[process_uuid].append({'idref': file_uuid, 'relationship': process_references_mapping[feature]}) + self.attributes['filename'][call['path']].add((process_uuid, file_references_mapping[feature])) def parse_fileinfo(self): fileinfo = self.data['fileinfo'] @@ -149,8 +159,7 @@ class JoeParser(): def parse_registryactivities(self, process_uuid, registryactivities): if registryactivities['keyCreated']: for call in registryactivities['keyCreated']['call']: - regkey_uuid = self.create_attribute(call, 'regkey') - self.references[process_uuid].append({'idref': regkey_uuid, 'relationship': 'creates'}) + self.attributes['regkey'][call['path']].add((process_uuid, 'creates')) for feature, relationship_type in registry_references_mapping.items(): if registryactivities[feature]: for call in registryactivities[feature]['call']: @@ -162,9 +171,9 @@ class JoeParser(): self.misp_event.add_object(**registry_key) self.references[process_uuid].append({'idref': registry_key.uuid, 'relationship': relationship_type}) - def create_attribute(self, field, attribute_type): + def create_attribute(self, attribute_type, attribute_value): attribute = MISPAttribute() - attribute.from_dict(**{'type': attribute_type, 'value': field['path']}) + attribute.from_dict(**{'type': attribute_type, 'value': attribute_value}) self.misp_event.add_attribute(**attribute) return attribute.uuid From 0d5f86782518878ef752707eed8d833b25d051fb Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Fri, 17 May 2019 22:18:11 +0200 Subject: [PATCH 134/207] add: Starting parsing network behavior fields --- misp_modules/modules/import_mod/joe_import.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/misp_modules/modules/import_mod/joe_import.py b/misp_modules/modules/import_mod/joe_import.py index 6275c50..0b854e3 100644 --- a/misp_modules/modules/import_mod/joe_import.py +++ b/misp_modules/modules/import_mod/joe_import.py @@ -21,6 +21,8 @@ file_object_mapping = {'entropy': ('float', 'entropy'), 'filetype': ('mime-type', 'mimetype')} file_references_mapping = {'fileCreated': 'creates', 'fileDeleted': 'deletes', 'fileMoved': 'moves', 'fileRead': 'reads', 'fileWritten': 'writes'} +network_connection_object_mapping = {'srcip': ('ip-src', 'ip-src'), 'dstip': ('ip-dst', 'ip-dst'), + 'srcport': ('port', 'src-port'), 'dstport': ('port', 'dst-port')} pe_object_fields = {'entrypoint': ('text', 'entrypoint-address'), 'imphash': ('imphash', 'imphash')} pe_object_mapping = {'CompanyName': 'company-name', 'FileDescription': 'file-description', @@ -81,6 +83,25 @@ class JoeParser(): def parse_behavior_network(self): network = self.data['behavior']['network'] + protocols = {'tcp': 4, 'udp': 4, 'icmp': 3, + 'http': 7, 'https': 7, 'ftp': 7} + fields = ('srcip', 'dstip', 'srcport', 'dstport') + for protocol, layer in protocols.items(): + if network.get(protocol): + connections = defaultdict(list) + for packet in network[protocol]['packet']: + timestamp = self.parse_timestamp(packet['timestamp']) + connections[(packet[field] for field in fields)].append(datetime.strptime(timestamp, '%B %d, %Y %H:%M:%S.%f')) + for connection, timestamps in connections.items(): + network_connection_object = MISPObject('network-connection') + for field, value in zip(fields, connection): + attribute_type, object_relation = network_connection_object_mapping[field] + network_connection_object.add_attribute(object_relation, **{'type': attribute_type, 'value': value}) + network_connection_object.add_attribute('first-packet-seen', **{'type': 'datetime', 'value': min(timestamps)}) + network_connection_object.add_attribute('layer{}-protocol'.format(layer), + **{'type': 'text', 'value': protocol}) + self.misp_event.add_object(**network_connection_object) + self.references[self.fileinfo_uuid].append({'idref': network_connection_object.uuid, 'relationship': 'initiates'}) def parse_behavior_system(self): system = self.data['behavior']['system'] @@ -181,6 +202,12 @@ class JoeParser(): event = json.loads(self.misp_event.to_json())['Event'] self.results = {key: event[key] for key in ('Attribute', 'Object') if (key in event and event[key])} + @staticmethod + def parse_timestamp(timestamp): + timestamp = timestamp.split(':') + timestamp[-1] = str(round(float(timestamp[-1].split(' ')[0]), 6)) + return ':'.join(timestamp) + def handler(q=False): if q is False: From 54f5fa6fa92b6410f343ab8f6430d23f6a419802 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 20 May 2019 09:19:38 +0200 Subject: [PATCH 135/207] fix: Avoiding dictionary indexes issues - Using tuples as a dictionary indexes is better than using generators... --- misp_modules/modules/import_mod/joe_import.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/import_mod/joe_import.py b/misp_modules/modules/import_mod/joe_import.py index 0b854e3..614d430 100644 --- a/misp_modules/modules/import_mod/joe_import.py +++ b/misp_modules/modules/import_mod/joe_import.py @@ -91,7 +91,7 @@ class JoeParser(): connections = defaultdict(list) for packet in network[protocol]['packet']: timestamp = self.parse_timestamp(packet['timestamp']) - connections[(packet[field] for field in fields)].append(datetime.strptime(timestamp, '%B %d, %Y %H:%M:%S.%f')) + connections[tuple(packet[field] for field in fields)].append(datetime.strptime(timestamp, '%B %d, %Y %H:%M:%S.%f')) for connection, timestamps in connections.items(): network_connection_object = MISPObject('network-connection') for field, value in zip(fields, connection): From 72e5f0099d322d90bf28d886a2276155ea6ec77c Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 20 May 2019 10:52:34 +0200 Subject: [PATCH 136/207] fix: Avoid creating a signer info object when the pe is not signed --- misp_modules/modules/import_mod/joe_import.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/misp_modules/modules/import_mod/joe_import.py b/misp_modules/modules/import_mod/joe_import.py index 614d430..e8d9e90 100644 --- a/misp_modules/modules/import_mod/joe_import.py +++ b/misp_modules/modules/import_mod/joe_import.py @@ -155,16 +155,18 @@ class JoeParser(): pe_object.add_attribute(pe_object_mapping[name], **{'type': 'text', 'value': feature['value']}) sections_number = len(peinfo['sections']['section']) pe_object.add_attribute('number-sections', **{'type': 'counter', 'value': sections_number}) - signerinfo_object = MISPObject('authenticode-signerinfo') - pe_object.add_reference(signerinfo_object.uuid, 'signed-by') - self.misp_event.add_object(**pe_object) - signerinfo_object.add_attribute('program-name', **{'type': 'text', 'value': program_name}) signatureinfo = peinfo['signature'] if signatureinfo['signed']: + signerinfo_object = MISPObject('authenticode-signerinfo') + pe_object.add_reference(signerinfo_object.uuid, 'signed-by') + self.misp_event.add_object(**pe_object) + signerinfo_object.add_attribute('program-name', **{'type': 'text', 'value': program_name}) for feature, mapping in signerinfo_object_mapping.items(): attribute_type, object_relation = mapping signerinfo_object.add_attribute(object_relation, **{'type': attribute_type, 'value': signatureinfo[feature]}) - self.misp_event.add_object(**signerinfo_object) + self.misp_event.add_object(**signerinfo_object) + else: + self.misp_event.add_object(**pe_object) for section in peinfo['sections']['section']: section_object = self.parse_pe_section(section) self.references[pe_object.uuid].append({'idref': section_object.uuid, 'relationship': 'included-in'}) From 417c306ace6a49c0199a1ab2ebcf717ae94babfe Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 20 May 2019 15:59:18 +0200 Subject: [PATCH 137/207] fix: Avoiding network connection object duplicates --- misp_modules/modules/import_mod/joe_import.py | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/misp_modules/modules/import_mod/joe_import.py b/misp_modules/modules/import_mod/joe_import.py index e8d9e90..2721362 100644 --- a/misp_modules/modules/import_mod/joe_import.py +++ b/misp_modules/modules/import_mod/joe_import.py @@ -21,6 +21,7 @@ file_object_mapping = {'entropy': ('float', 'entropy'), 'filetype': ('mime-type', 'mimetype')} file_references_mapping = {'fileCreated': 'creates', 'fileDeleted': 'deletes', 'fileMoved': 'moves', 'fileRead': 'reads', 'fileWritten': 'writes'} +network_behavior_fields = ('srcip', 'dstip', 'srcport', 'dstport') network_connection_object_mapping = {'srcip': ('ip-src', 'ip-src'), 'dstip': ('ip-dst', 'ip-dst'), 'srcport': ('port', 'src-port'), 'dstport': ('port', 'dst-port')} pe_object_fields = {'entrypoint': ('text', 'entrypoint-address'), @@ -33,6 +34,8 @@ pe_object_mapping = {'CompanyName': 'company-name', 'FileDescription': 'file-des process_object_fields = {'cmdline': 'command-line', 'name': 'name', 'parentpid': 'parent-pid', 'pid': 'pid', 'path': 'current-directory'} +protocols = {'tcp': 4, 'udp': 4, 'icmp': 3, + 'http': 7, 'https': 7, 'ftp': 7} section_object_mapping = {'characteristics': ('text', 'characteristic'), 'entropy': ('float', 'entropy'), 'name': ('text', 'name'), 'rawaddr': ('hex', 'offset'), @@ -83,23 +86,31 @@ class JoeParser(): def parse_behavior_network(self): network = self.data['behavior']['network'] - protocols = {'tcp': 4, 'udp': 4, 'icmp': 3, - 'http': 7, 'https': 7, 'ftp': 7} - fields = ('srcip', 'dstip', 'srcport', 'dstport') + connections = defaultdict(lambda: defaultdict(set)) for protocol, layer in protocols.items(): if network.get(protocol): - connections = defaultdict(list) for packet in network[protocol]['packet']: - timestamp = self.parse_timestamp(packet['timestamp']) - connections[tuple(packet[field] for field in fields)].append(datetime.strptime(timestamp, '%B %d, %Y %H:%M:%S.%f')) - for connection, timestamps in connections.items(): + timestamp = datetime.strptime(self.parse_timestamp(packet['timestamp']), '%B %d, %Y %H:%M:%S.%f') + connections[tuple(packet[field] for field in network_behavior_fields)][protocol].add(timestamp) + for connection, data in connections.items(): + attributes = self.prefetch_attributes_data(connection) + if len(data.keys()) == len(set(protocols[protocol] for protocol in data.keys())): + network_connection_object = MISPObject('network-connection') + for object_relation, attribute in attributes.items(): + network_connection_object.add_attribute(object_relation, **attribute) + network_connection_object.add_attribute('first-packet-seen', + **{'type': 'datetime', 'value': min(tuple(min(timestamp) for timestamp in data.values()))}) + for protocol in data.keys(): + network_connection_object.add_attribute('layer{}-protocol'.format(protocols[protocol]), **{'type': 'text', 'value': protocol}) + self.misp_event.add_object(**network_connection_object) + self.references[self.fileinfo_uuid].append({'idref': network_connection_object.uuid, 'relationship': 'initiates'}) + else: + for protocol, timestamps in data.items(): network_connection_object = MISPObject('network-connection') - for field, value in zip(fields, connection): - attribute_type, object_relation = network_connection_object_mapping[field] - network_connection_object.add_attribute(object_relation, **{'type': attribute_type, 'value': value}) + for object_relation, attribute in attributes.items(): + network_connection_object.add_attribute(object_relation, **attribute) network_connection_object.add_attribute('first-packet-seen', **{'type': 'datetime', 'value': min(timestamps)}) - network_connection_object.add_attribute('layer{}-protocol'.format(layer), - **{'type': 'text', 'value': protocol}) + network_connection_object.add_attribute('layer{}-protocol'.format(protocols[protocol]), **{'type': 'text', 'value': protocol}) self.misp_event.add_object(**network_connection_object) self.references[self.fileinfo_uuid].append({'idref': network_connection_object.uuid, 'relationship': 'initiates'}) @@ -210,6 +221,14 @@ class JoeParser(): timestamp[-1] = str(round(float(timestamp[-1].split(' ')[0]), 6)) return ':'.join(timestamp) + @staticmethod + def prefetch_attributes_data(connection): + attributes = {} + for field, value in zip(network_behavior_fields, connection): + attribute_type, object_relation = network_connection_object_mapping[field] + attributes[object_relation] = {'type': attribute_type, 'value': value} + return attributes + def handler(q=False): if q is False: From 1745d33ee42b37ff52477262588c50a765f52b29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Georg=20Sch=C3=B6lly?= Date: Tue, 21 May 2019 21:14:21 +0200 Subject: [PATCH 138/207] add expansion for joe sandbox --- REQUIREMENTS | 1 + misp_modules/modules/expansion/__init__.py | 2 +- .../modules/expansion/joesandbox_submit.py | 140 ++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 misp_modules/modules/expansion/joesandbox_submit.py diff --git a/REQUIREMENTS b/REQUIREMENTS index 9447c47..1d17946 100644 --- a/REQUIREMENTS +++ b/REQUIREMENTS @@ -30,6 +30,7 @@ httplib2==0.12.3 idna-ssl==1.1.0 ; python_version < '3.7' idna==2.8 isodate==0.6.0 +jbxapi==3.1.3 jsonschema==3.0.1 lxml==4.3.3 maclookup==1.0.3 diff --git a/misp_modules/modules/expansion/__init__.py b/misp_modules/modules/expansion/__init__.py index 70aca68..e5d17c1 100644 --- a/misp_modules/modules/expansion/__init__.py +++ b/misp_modules/modules/expansion/__init__.py @@ -10,4 +10,4 @@ __all__ = ['cuckoo_submit', 'vmray_submit', 'bgpranking', 'circl_passivedns', 'c 'sigma_queries', 'dbl_spamhaus', 'vulners', 'yara_query', 'macaddress_io', 'intel471', 'backscatter_io', 'btc_scam_check', 'hibp', 'greynoise', 'macvendors', 'qrcode', 'ocr-enrich', 'pdf-enrich', 'docx-enrich', 'xlsx-enrich', 'pptx-enrich', - 'ods-enrich', 'odt-enrich'] + 'ods-enrich', 'odt-enrich', 'joesandbox_submit'] diff --git a/misp_modules/modules/expansion/joesandbox_submit.py b/misp_modules/modules/expansion/joesandbox_submit.py new file mode 100644 index 0000000..39b140e --- /dev/null +++ b/misp_modules/modules/expansion/joesandbox_submit.py @@ -0,0 +1,140 @@ +import jbxapi +import base64 +import io +import json +import logging +import sys +import zipfile +import re + +from urllib.parse import urljoin + + +log = logging.getLogger(__name__) +log.setLevel(logging.DEBUG) +sh = logging.StreamHandler(sys.stdout) +sh.setLevel(logging.DEBUG) +fmt = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +sh.setFormatter(fmt) +log.addHandler(sh) + +moduleinfo = { + "version": "1.0", + "author": "Joe Security LLC", + "description": "Submit files and URLs to Joe Sandbox", + "module-type": ["expansion", "hover"] +} +moduleconfig = [ + "apiurl", + "apikey", + "accept-tac", + "report-cache", + "systems", +] + +mispattributes = { + "input": ["attachment", "malware-sample", "url", "domain"], + "output": ["link"], +} + + +def handler(q=False): + if q is False: + return False + + request = json.loads(q) + + apiurl = request["config"].get("apiurl") or "https://jbxcloud.joesecurity.org/api" + apikey = request["config"].get("apikey") + + # systems + systems = request["config"].get("systems") or "" + systems = [s.strip() for s in re.split(r"[\s,;]", systems) if s.strip()] + + try: + accept_tac = _parse_bool(request["config"].get("accept-tac"), "accept-tac") + report_cache = _parse_bool(request["config"].get("report-cache"), "report-cache") + except _ParseError as e: + return {"error": str(e)} + + params = { + "report-cache": report_cache, + "systems": systems, + } + + if not apikey: + return {"error": "No API key provided"} + + joe = jbxapi.JoeSandbox(apiurl=apiurl, apikey=apikey, user_agent="MISP joesandbox_submit", accept_tac=accept_tac) + + try: + is_url_submission = "url" in request or "domain" in request + + if is_url_submission: + url = request.get("url") or request.get("domain") + + log.info("Submitting URL: %s", url) + result = joe.submit_url(url, params=params) + else: + if "malware-sample" in request: + filename = request.get("malware-sample").split("|", 1)[0] + data = _decode_malware(request["data"], True) + elif "attachment" in request: + filename = request["attachment"] + data = _decode_malware(request["data"], False) + + data_fp = io.BytesIO(data) + log.info("Submitting sample: %s", filename) + result = joe.submit_sample((filename, data_fp), params=params) + + assert "submission_id" in result + except jbxapi.JoeException as e: + return {"error": str(e)} + + link_to_analysis = urljoin(apiurl, "../submissions/{}".format(result["submission_id"])) + + return { + "results": [{ + "types": "link", + "categories": "External analysis", + "values": link_to_analysis, + }] + } + + +def introspection(): + return mispattributes + + +def version(): + moduleinfo["config"] = moduleconfig + return moduleinfo + + +def _decode_malware(data, is_encrypted): + data = base64.b64decode(data) + + if is_encrypted: + with zipfile.ZipFile(io.BytesIO(data)) as zipf: + data = zipf.read(zipf.namelist()[0], pwd=b"infected") + + return data + + +class _ParseError(Exception): + pass + + +def _parse_bool(value, name="bool"): + if value is None or value == "": + return None + + if value == "true": + return True + + if value == "false": + return False + + raise _ParseError("Cannot parse {}. Must be 'true' or 'false'".format(name)) From 191034d31111c447c7c0df31793279a910f36434 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Tue, 21 May 2019 23:37:53 +0200 Subject: [PATCH 139/207] add: Starting parsing dropped files --- misp_modules/modules/import_mod/joe_import.py | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/misp_modules/modules/import_mod/joe_import.py b/misp_modules/modules/import_mod/joe_import.py index 2721362..237218d 100644 --- a/misp_modules/modules/import_mod/joe_import.py +++ b/misp_modules/modules/import_mod/joe_import.py @@ -15,6 +15,11 @@ moduleinfo = {'version': '0.1', 'author': 'Christian Studer', moduleconfig = [] +dropped_file_mapping = {'@entropy': ('float', 'entropy'), + '@file': ('filename', 'filename'), + '@size': ('size-in-bytes', 'size-in-bytes'), + '@type': ('mime-type', 'mimetype')} +dropped_hash_mapping = {'MD5': 'md5', 'SHA': 'sha1', 'SHA-256': 'sha256', 'SHA-512': 'sha512'} file_object_fields = ['filename', 'md5', 'sha1', 'sha256', 'sha512', 'ssdeep'] file_object_mapping = {'entropy': ('float', 'entropy'), 'filesize': ('size-in-bytes', 'size-in-bytes'), @@ -58,7 +63,9 @@ class JoeParser(): def parse_joe(self): self.parse_fileinfo() - self.parse_behavior() + self.parse_system_behavior() + self.parse_network_behavior() + self.parse_dropped_files() if self.attributes: self.handle_attributes() if self.references: @@ -80,11 +87,22 @@ class JoeParser(): source_uuid, relationship = reference self.references[source_uuid].append({'idref': attribute_uuid, 'relationship': relationship}) - def parse_behavior(self): - self.parse_behavior_system() - self.parse_behavior_network() + def parse_dropped_files(self): + droppedinfo = self.data['droppedinfo'] + if droppedinfo: + for droppedfile in droppedinfo['hash']: + file_object = MISPObject('file') + for key, mapping in dropped_file_mapping.items(): + attribute_type, object_relation = mapping + file_object.add_attribute(object_relation, **{'type': attribute_type, 'value': droppedfile[key]}) + if droppedfile['@malicious'] == 'true': + file_object.add_attribute('state', **{'type': 'text', 'value': 'Malicious'}) + for h in droppedfile['value']: + hash_type = dropped_hash_mapping[h['@algo']] + file_object.add_attribute(hash_type, **{'type': hash_type, 'value': h['$']}) + self.misp_event.add_object(**file_object) - def parse_behavior_network(self): + def parse_network_behavior(self): network = self.data['behavior']['network'] connections = defaultdict(lambda: defaultdict(set)) for protocol, layer in protocols.items(): @@ -114,7 +132,7 @@ class JoeParser(): self.misp_event.add_object(**network_connection_object) self.references[self.fileinfo_uuid].append({'idref': network_connection_object.uuid, 'relationship': 'initiates'}) - def parse_behavior_system(self): + def parse_system_behavior(self): system = self.data['behavior']['system'] if system.get('processes'): process_activities = {'fileactivities': self.parse_fileactivities, From cfec9a6b1ca7aa811f3ffddef83124489b9050aa Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Wed, 22 May 2019 15:27:04 +0200 Subject: [PATCH 140/207] fix: Added references between processes and the files they drop --- misp_modules/modules/import_mod/joe_import.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/misp_modules/modules/import_mod/joe_import.py b/misp_modules/modules/import_mod/joe_import.py index 237218d..c70531c 100644 --- a/misp_modules/modules/import_mod/joe_import.py +++ b/misp_modules/modules/import_mod/joe_import.py @@ -60,6 +60,7 @@ class JoeParser(): self.misp_event = MISPEvent() self.references = defaultdict(list) self.attributes = defaultdict(lambda: defaultdict(set)) + self.process_references = {} def parse_joe(self): self.parse_fileinfo() @@ -101,6 +102,10 @@ class JoeParser(): hash_type = dropped_hash_mapping[h['@algo']] file_object.add_attribute(hash_type, **{'type': hash_type, 'value': h['$']}) self.misp_event.add_object(**file_object) + self.references[self.process_references[(int(droppedfile['@targetid']), droppedfile['@process'])]].append({ + 'idref': file_object.uuid, + 'relationship': 'drops' + }) def parse_network_behavior(self): network = self.data['behavior']['network'] @@ -148,6 +153,7 @@ class JoeParser(): for field, to_call in process_activities.items(): to_call(process_object.uuid, process[field]) self.references[self.fileinfo_uuid].append({'idref': process_object.uuid, 'relationship': 'calls'}) + self.process_references[(general['targetid'], general['path'])] = process_object.uuid def parse_fileactivities(self, process_uuid, fileactivities): for feature, files in fileactivities.items(): From e608107a09b489f2cfcee0d70bfe6a3ff28fa4e6 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Wed, 22 May 2019 17:12:49 +0200 Subject: [PATCH 141/207] add: Parsing domains, urls & ips contacted by processes --- misp_modules/modules/import_mod/joe_import.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/misp_modules/modules/import_mod/joe_import.py b/misp_modules/modules/import_mod/joe_import.py index c70531c..efefb3e 100644 --- a/misp_modules/modules/import_mod/joe_import.py +++ b/misp_modules/modules/import_mod/joe_import.py @@ -15,6 +15,7 @@ moduleinfo = {'version': '0.1', 'author': 'Christian Studer', moduleconfig = [] +domain_object_mapping = {'@ip': ('ip-dst', 'ip'), '@name': ('domain', 'domain')} dropped_file_mapping = {'@entropy': ('float', 'entropy'), '@file': ('filename', 'filename'), '@size': ('size-in-bytes', 'size-in-bytes'), @@ -66,6 +67,7 @@ class JoeParser(): self.parse_fileinfo() self.parse_system_behavior() self.parse_network_behavior() + self.parse_network_interactions() self.parse_dropped_files() if self.attributes: self.handle_attributes() @@ -207,6 +209,47 @@ class JoeParser(): self.references[pe_object.uuid].append({'idref': section_object.uuid, 'relationship': 'included-in'}) self.misp_event.add_object(**section_object) + def parse_network_interactions(self): + domaininfo = self.data['domaininfo'] + if domaininfo: + for domain in domaininfo['domain']: + domain_object = MISPObject('domain-ip') + for key, mapping in domain_object_mapping.items(): + attribute_type, object_relation = mapping + domain_object.add_attribute(object_relation, **{'type': attribute_type, 'value': domain[key]}) + self.misp_event.add_object(**domain_object) + self.references[self.process_references[(int(domain['@targetid']), domain['@currentpath'])]].append({ + 'idref': domain_object.uuid, + 'relationship': 'contacts' + }) + ipinfo = self.data['ipinfo'] + if ipinfo: + for ip in ipinfo['ip']: + attribute = MISPAttribute() + attribute.from_dict(**{'type': 'ip-dst', 'value': ip['@ip']}) + self.misp_event.add_attribute(**attribute) + self.references[self.process_references[(int(ip['@targetid']), ip['@currentpath'])]].append({ + 'idref': attribute.uuid, + 'relationship': 'contacts' + }) + urlinfo = self.data['urlinfo'] + if urlinfo: + for url in urlinfo['url']: + target_id = int(url['@targetid']) + current_path = url['@currentpath'] + attribute = MISPAttribute() + attribute_dict = {'type': 'url', 'value': url['@name']} + if target_id != -1 and current_path != 'unknown': + self.references[self.process_references[(target_id, current_path)]].append({ + 'idref': attribute.uuid, + 'relationship': 'contacts' + }) + else: + attribute_dict['comment'] = 'From Memory - Enriched via the joe_import module' + attribute.from_dict(**attribute_dict) + self.misp_event.add_attribute(**attribute) + + def parse_pe_section(self, section): section_object = MISPObject('pe-section') for feature, mapping in section_object_mapping.items(): From be05de62c008e57f4ac322e045ff6f52b8dac05c Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Thu, 23 May 2019 15:59:52 +0200 Subject: [PATCH 142/207] add: Parsing MITRE ATT&CK tactic matrix related to the Joe report --- misp_modules/modules/import_mod/joe_import.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/misp_modules/modules/import_mod/joe_import.py b/misp_modules/modules/import_mod/joe_import.py index efefb3e..d20de60 100644 --- a/misp_modules/modules/import_mod/joe_import.py +++ b/misp_modules/modules/import_mod/joe_import.py @@ -73,6 +73,7 @@ class JoeParser(): self.handle_attributes() if self.references: self.build_references() + self.parse_mitre_attack() self.finalize_results() def build_references(self): @@ -109,6 +110,14 @@ class JoeParser(): 'relationship': 'drops' }) + def parse_mitre_attack(self): + mitreattack = self.data['mitreattack'] + if mitreattack: + for tactic in mitreattack['tactic']: + if tactic.get('technique'): + for technique in tactic['technique']: + self.misp_event.add_tag('misp-galaxy:mitre-attack-pattern="{} - {}"'.format(technique['name'], technique['id'])) + def parse_network_behavior(self): network = self.data['behavior']['network'] connections = defaultdict(lambda: defaultdict(set)) From 8ac651562e215077ef7eb5333a409ac78e250223 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Thu, 23 May 2019 16:13:49 +0200 Subject: [PATCH 143/207] fix: Making pep8 & travis happy --- misp_modules/modules/import_mod/joe_import.py | 1 - 1 file changed, 1 deletion(-) diff --git a/misp_modules/modules/import_mod/joe_import.py b/misp_modules/modules/import_mod/joe_import.py index d20de60..0d22074 100644 --- a/misp_modules/modules/import_mod/joe_import.py +++ b/misp_modules/modules/import_mod/joe_import.py @@ -258,7 +258,6 @@ class JoeParser(): attribute.from_dict(**attribute_dict) self.misp_event.add_attribute(**attribute) - def parse_pe_section(self, section): section_object = MISPObject('pe-section') for feature, mapping in section_object_mapping.items(): From 2cd11ba497e8f56d84fd24cfac3224d6147cbfec Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Sat, 25 May 2019 09:00:23 +0200 Subject: [PATCH 144/207] chg: [requirements] Python API wrapper for the Joe Sandbox API added --- Pipfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Pipfile b/Pipfile index 36f059e..9856955 100644 --- a/Pipfile +++ b/Pipfile @@ -55,6 +55,7 @@ pdftotext = "*" lxml = "*" xlrd = "*" idna-ssl = {markers="python_version < '3.7'"} +jbxapi = "*" [requires] python_version = "3" From 74f1de15e399312da5659154a4f8ee0675e0f4dc Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Sat, 25 May 2019 09:20:54 +0200 Subject: [PATCH 145/207] chg: [install] Pipfile.lock updated --- Pipfile.lock | 60 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 9f395cf..5570331 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9aac0a9c45df16b9502c13f9468095cf5ffdb8bc407fe2b55faee3ff53d8eba3" + "sha256": "3b1ae107ffee673cfabae67742774ee8ebdc3b82313608b529c2c4cf4a41ddc9" }, "pipfile-spec": 6, "requires": { @@ -192,6 +192,13 @@ ], "version": "==0.6.0" }, + "jbxapi": { + "hashes": [ + "sha256:ff7c74b3cc06aebd3f2d99a1ffb042b842d527faff1d6006f6224907fcf6ce6f" + ], + "index": "pypi", + "version": "==3.1.3" + }, "jsonschema": { "hashes": [ "sha256:0c0a81564f181de3212efa2d17de1910f8732fa1b71c42266d983cd74304e20d", @@ -455,7 +462,7 @@ "pybgpranking": { "editable": true, "git": "https://github.com/D4-project/BGP-Ranking.git/", - "ref": "aa0d4581a836503d2298ee046bea49c501eefdd1", + "ref": "429cea9c0787876820984a2df4e982449a84c10e", "subdirectory": "client" }, "pydnstrails": { @@ -492,7 +499,7 @@ "pymisp": { "editable": true, "git": "https://github.com/MISP/PyMISP.git", - "ref": "e8bba395bc67bf56e41ddd022ebae670c5b0d64b" + "ref": "583fb6592495ea358aad47a8a1ec92d43c13348a" }, "pyonyphe": { "editable": true, @@ -523,9 +530,9 @@ }, "pyrsistent": { "hashes": [ - "sha256:5403d37f4d55ff4572b5b5676890589f367a9569529c6f728c11046c4ea4272b" + "sha256:16692ee739d42cf5e39cef8d27649a8c1fdb7aa99887098f1460057c5eb75c3a" ], - "version": "==0.15.1" + "version": "==0.15.2" }, "pytesseract": { "hashes": [ @@ -637,11 +644,11 @@ }, "requests": { "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", + "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" ], "index": "pypi", - "version": "==2.21.0" + "version": "==2.22.0" }, "requests-cache": { "hashes": [ @@ -728,10 +735,10 @@ }, "urllib3": { "hashes": [ - "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", - "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" + "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", + "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232" ], - "version": "==1.24.3" + "version": "==1.25.3" }, "uwhois": { "editable": true, @@ -927,10 +934,10 @@ }, "pluggy": { "hashes": [ - "sha256:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f", - "sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746" + "sha256:25a1bc1d148c9a640211872b4ff859878d422bccb59c9965e04eed468a0aa180", + "sha256:964cedd2b27c492fbf0b7f58b3284a09cf7f99b0f715941fb24a439b3af1bd1a" ], - "version": "==0.9.0" + "version": "==0.11.0" }, "py": { "hashes": [ @@ -955,19 +962,19 @@ }, "pytest": { "hashes": [ - "sha256:3773f4c235918987d51daf1db66d51c99fac654c81d6f2f709a046ab446d5e5d", - "sha256:b7802283b70ca24d7119b32915efa7c409982f59913c1a6c0640aacf118b95f5" + "sha256:1a8aa4fa958f8f451ac5441f3ac130d9fc86ea38780dd2715e6d5c5882700b24", + "sha256:b8bf138592384bd4e87338cb0f256bf5f615398a649d4bd83915f0e4047a5ca6" ], "index": "pypi", - "version": "==4.4.1" + "version": "==4.5.0" }, "requests": { "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", + "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" ], "index": "pypi", - "version": "==2.21.0" + "version": "==2.22.0" }, "six": { "hashes": [ @@ -978,10 +985,17 @@ }, "urllib3": { "hashes": [ - "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", - "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" + "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", + "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232" ], - "version": "==1.24.3" + "version": "==1.25.3" + }, + "wcwidth": { + "hashes": [ + "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", + "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + ], + "version": "==0.1.7" } } } From feeca026254a88760572b817b6320ad429639f99 Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Sat, 25 May 2019 09:21:59 +0200 Subject: [PATCH 146/207] chg: [install] REQUIREMENTS file updated --- REQUIREMENTS | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/REQUIREMENTS b/REQUIREMENTS index 9447c47..1a9c146 100644 --- a/REQUIREMENTS +++ b/REQUIREMENTS @@ -1,9 +1,9 @@ -i https://pypi.org/simple -e . --e git+https://github.com/D4-project/BGP-Ranking.git/@4e0741056bcc0077de1120b8724a31330b26033e#egg=pybgpranking&subdirectory=client --e git+https://github.com/D4-project/IPASN-History.git/@c0c2bbf8d70811982dad065ea463a7e01593a38d#egg=pyipasnhistory&subdirectory=client +-e git+https://github.com/D4-project/BGP-Ranking.git/@429cea9c0787876820984a2df4e982449a84c10e#egg=pybgpranking&subdirectory=client +-e git+https://github.com/D4-project/IPASN-History.git/@47cd0f2658ab172fce42126ff3a1dbcddfb0b5fb#egg=pyipasnhistory&subdirectory=client -e git+https://github.com/MISP/PyIntel471.git@0df8d51f1c1425de66714b3a5a45edb69b8cc2fc#egg=pyintel471 --e git+https://github.com/MISP/PyMISP.git@582dda0ce2a8ca8e1dd2cf3842e0491caca51c62#egg=pymisp +-e git+https://github.com/MISP/PyMISP.git@583fb6592495ea358aad47a8a1ec92d43c13348a#egg=pymisp -e git+https://github.com/Rafiot/uwhoisd.git@411572840eba4c72dc321c549b36a54ed5cea9de#egg=uwhois&subdirectory=client -e git+https://github.com/cartertemm/ODTReader.git/@49d6938693f6faa3ff09998f86dba551ae3a996b#egg=odtreader -e git+https://github.com/sebdraven/pydnstrails@48c1f740025c51289f43a24863d1845ff12fd21a#egg=pydnstrails @@ -30,6 +30,7 @@ httplib2==0.12.3 idna-ssl==1.1.0 ; python_version < '3.7' idna==2.8 isodate==0.6.0 +jbxapi==3.1.3 jsonschema==3.0.1 lxml==4.3.3 maclookup==1.0.3 @@ -47,22 +48,22 @@ psutil==5.6.2 pyeupi==1.0 pygeoip==0.3.2 pyparsing==2.4.0 -pypdns==1.3 +pypdns==1.4.1 pypssl==2.1 -pyrsistent==0.15.1 +pyrsistent==0.15.2 pytesseract==0.2.6 python-dateutil==2.8.0 python-docx==0.8.10 -python-pptx==0.6.17 +python-pptx==0.6.18 pytz==2019.1 pyyaml==5.1 pyzbar==0.1.8 rdflib==4.2.2 redis==3.2.1 -reportlab==3.5.20 +reportlab==3.5.21 requests-cache==0.5.0 -requests==2.21.0 -shodan==1.12.1 +requests==2.22.0 +shodan==1.13.0 sigmatools==0.10 six==1.12.0 soupsieve==1.9.1 @@ -72,10 +73,10 @@ tabulate==0.8.3 tornado==6.0.2 url-normalize==1.4.1 urlarchiver==0.2 -urllib3==1.24.2 +urllib3==1.25.3 vulners==1.5.0 wand==0.5.3 xlrd==1.2.0 -xlsxwriter==1.1.7 +xlsxwriter==1.1.8 yara-python==3.8.1 yarl==1.3.0 From 2060d02f18d45d6e205696c5f07d834d215cba19 Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Sat, 25 May 2019 09:37:23 +0200 Subject: [PATCH 147/207] new: [doc] Joe Sandbox added in the list --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6ec5bc6..ecce406 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ * [intel471](misp_modules/modules/expansion/intel471.py) - an expansion module to get info from [Intel471](https://intel471.com). * [IPASN](misp_modules/modules/expansion/ipasn.py) - a hover and expansion to get the BGP ASN of an IP address. * [iprep](misp_modules/modules/expansion/iprep.py) - an expansion module to get IP reputation from packetmail.net. +* [Joe Sandbox](misp_modules/modules/expansion/joesandbox_submit.py) - Submit files and URLs to Joe Sandbox. * [macaddress.io](misp_modules/modules/expansion/macaddress_io.py) - a hover module to retrieve vendor details and other information regarding a given MAC address or an OUI from [MAC address Vendor Lookup](https://macaddress.io). See [integration tutorial here](https://macaddress.io/integrations/MISP-module). * [macvendors](misp_modules/modules/expansion/macvendors.py) - a hover module to retrieve mac vendor information. * [ocr-enrich](misp_modules/modules/expansion/ocr-enrich.py) - an enrichment module to get OCRized data from images into MISP. From 380b8d46ba6673a97b4a3043332e0285ec8a98c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Georg=20Sch=C3=B6lly?= Date: Tue, 28 May 2019 11:19:32 +0200 Subject: [PATCH 148/207] improve forwards-compatibility --- misp_modules/modules/import_mod/joe_import.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/misp_modules/modules/import_mod/joe_import.py b/misp_modules/modules/import_mod/joe_import.py index 0d22074..8f258cd 100644 --- a/misp_modules/modules/import_mod/joe_import.py +++ b/misp_modules/modules/import_mod/joe_import.py @@ -168,6 +168,10 @@ class JoeParser(): def parse_fileactivities(self, process_uuid, fileactivities): for feature, files in fileactivities.items(): + # ignore unknown features + if feature not in file_references_mapping: + continue + if files: for call in files['call']: self.attributes['filename'][call['path']].add((process_uuid, file_references_mapping[feature])) @@ -198,7 +202,8 @@ class JoeParser(): name = feature['name'] if name == 'InternalName': program_name = feature['value'] - pe_object.add_attribute(pe_object_mapping[name], **{'type': 'text', 'value': feature['value']}) + if name in pe_object_mapping: + pe_object.add_attribute(pe_object_mapping[name], **{'type': 'text', 'value': feature['value']}) sections_number = len(peinfo['sections']['section']) pe_object.add_attribute('number-sections', **{'type': 'counter', 'value': sections_number}) signatureinfo = peinfo['signature'] From 9377a892f4d6ed4d5540b0b41fc4f12dbbbf2991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Georg=20Sch=C3=B6lly?= Date: Tue, 28 May 2019 11:20:00 +0200 Subject: [PATCH 149/207] support url analyses --- misp_modules/modules/import_mod/joe_import.py | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/misp_modules/modules/import_mod/joe_import.py b/misp_modules/modules/import_mod/joe_import.py index 8f258cd..2dc8990 100644 --- a/misp_modules/modules/import_mod/joe_import.py +++ b/misp_modules/modules/import_mod/joe_import.py @@ -64,11 +64,16 @@ class JoeParser(): self.process_references = {} def parse_joe(self): - self.parse_fileinfo() + if self.analysis_type() == "file": + self.parse_fileinfo() + else: + self.parse_url_analysis() + self.parse_system_behavior() self.parse_network_behavior() self.parse_network_interactions() self.parse_dropped_files() + if self.attributes: self.handle_attributes() if self.references: @@ -137,7 +142,7 @@ class JoeParser(): for protocol in data.keys(): network_connection_object.add_attribute('layer{}-protocol'.format(protocols[protocol]), **{'type': 'text', 'value': protocol}) self.misp_event.add_object(**network_connection_object) - self.references[self.fileinfo_uuid].append({'idref': network_connection_object.uuid, 'relationship': 'initiates'}) + self.references[self.analysisinfo_uuid].append({'idref': network_connection_object.uuid, 'relationship': 'initiates'}) else: for protocol, timestamps in data.items(): network_connection_object = MISPObject('network-connection') @@ -146,7 +151,7 @@ class JoeParser(): network_connection_object.add_attribute('first-packet-seen', **{'type': 'datetime', 'value': min(timestamps)}) network_connection_object.add_attribute('layer{}-protocol'.format(protocols[protocol]), **{'type': 'text', 'value': protocol}) self.misp_event.add_object(**network_connection_object) - self.references[self.fileinfo_uuid].append({'idref': network_connection_object.uuid, 'relationship': 'initiates'}) + self.references[self.analysisinfo_uuid].append({'idref': network_connection_object.uuid, 'relationship': 'initiates'}) def parse_system_behavior(self): system = self.data['behavior']['system'] @@ -163,7 +168,7 @@ class JoeParser(): self.misp_event.add_object(**process_object) for field, to_call in process_activities.items(): to_call(process_object.uuid, process[field]) - self.references[self.fileinfo_uuid].append({'idref': process_object.uuid, 'relationship': 'calls'}) + self.references[self.analysisinfo_uuid].append({'idref': process_object.uuid, 'relationship': 'calls'}) self.process_references[(general['targetid'], general['path'])] = process_object.uuid def parse_fileactivities(self, process_uuid, fileactivities): @@ -176,15 +181,36 @@ class JoeParser(): for call in files['call']: self.attributes['filename'][call['path']].add((process_uuid, file_references_mapping[feature])) + def analysis_type(self): + generalinfo = self.data['generalinfo'] + + if generalinfo['target']['sample']: + return "file" + elif generalinfo['target']['url']: + return "url" + else: + raise Exception("Unknown analysis type") + + def parse_url_analysis(self): + generalinfo = self.data["generalinfo"] + + url_object = MISPObject("url") + self.analysisinfo_uuid = url_object.uuid + + url_object.add_attribute("url", generalinfo["target"]["url"]) + self.misp_event.add_object(**url_object) + def parse_fileinfo(self): fileinfo = self.data['fileinfo'] + file_object = MISPObject('file') + self.analysisinfo_uuid = file_object.uuid + for field in file_object_fields: file_object.add_attribute(field, **{'type': field, 'value': fileinfo[field]}) for field, mapping in file_object_mapping.items(): attribute_type, object_relation = mapping file_object.add_attribute(object_relation, **{'type': attribute_type, 'value': fileinfo[field]}) - self.fileinfo_uuid = file_object.uuid if not fileinfo.get('pe'): self.misp_event.add_object(**file_object) return From 74b73f93328b50836349e8d59b59133b11ecbfbe Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Wed, 29 May 2019 11:26:14 +1000 Subject: [PATCH 150/207] chg: Moved JoeParser class to make it reachable from expansion & import modules --- misp_modules/lib/__init__.py | 1 + misp_modules/lib/joe_parser.py | 326 +++++++++++++++++ misp_modules/modules/import_mod/joe_import.py | 329 +----------------- 3 files changed, 332 insertions(+), 324 deletions(-) create mode 100644 misp_modules/lib/__init__.py create mode 100644 misp_modules/lib/joe_parser.py diff --git a/misp_modules/lib/__init__.py b/misp_modules/lib/__init__.py new file mode 100644 index 0000000..0dbceb8 --- /dev/null +++ b/misp_modules/lib/__init__.py @@ -0,0 +1 @@ +all = ['joe_parser'] diff --git a/misp_modules/lib/joe_parser.py b/misp_modules/lib/joe_parser.py new file mode 100644 index 0000000..a3dc82c --- /dev/null +++ b/misp_modules/lib/joe_parser.py @@ -0,0 +1,326 @@ +# -*- coding: utf-8 -*- +from collections import defaultdict +from datetime import datetime +from pymisp import MISPAttribute, MISPEvent, MISPObject +import json + + +domain_object_mapping = {'@ip': ('ip-dst', 'ip'), '@name': ('domain', 'domain')} +dropped_file_mapping = {'@entropy': ('float', 'entropy'), + '@file': ('filename', 'filename'), + '@size': ('size-in-bytes', 'size-in-bytes'), + '@type': ('mime-type', 'mimetype')} +dropped_hash_mapping = {'MD5': 'md5', 'SHA': 'sha1', 'SHA-256': 'sha256', 'SHA-512': 'sha512'} +file_object_fields = ['filename', 'md5', 'sha1', 'sha256', 'sha512', 'ssdeep'] +file_object_mapping = {'entropy': ('float', 'entropy'), + 'filesize': ('size-in-bytes', 'size-in-bytes'), + 'filetype': ('mime-type', 'mimetype')} +file_references_mapping = {'fileCreated': 'creates', 'fileDeleted': 'deletes', + 'fileMoved': 'moves', 'fileRead': 'reads', 'fileWritten': 'writes'} +network_behavior_fields = ('srcip', 'dstip', 'srcport', 'dstport') +network_connection_object_mapping = {'srcip': ('ip-src', 'ip-src'), 'dstip': ('ip-dst', 'ip-dst'), + 'srcport': ('port', 'src-port'), 'dstport': ('port', 'dst-port')} +pe_object_fields = {'entrypoint': ('text', 'entrypoint-address'), + 'imphash': ('imphash', 'imphash')} +pe_object_mapping = {'CompanyName': 'company-name', 'FileDescription': 'file-description', + 'FileVersion': 'file-version', 'InternalName': 'internal-filename', + 'LegalCopyright': 'legal-copyright', 'OriginalFilename': 'original-filename', + 'ProductName': 'product-filename', 'ProductVersion': 'product-version', + 'Translation': 'lang-id'} +process_object_fields = {'cmdline': 'command-line', 'name': 'name', + 'parentpid': 'parent-pid', 'pid': 'pid', + 'path': 'current-directory'} +protocols = {'tcp': 4, 'udp': 4, 'icmp': 3, + 'http': 7, 'https': 7, 'ftp': 7} +section_object_mapping = {'characteristics': ('text', 'characteristic'), + 'entropy': ('float', 'entropy'), + 'name': ('text', 'name'), 'rawaddr': ('hex', 'offset'), + 'rawsize': ('size-in-bytes', 'size-in-bytes'), + 'virtaddr': ('hex', 'virtual_address'), + 'virtsize': ('size-in-bytes', 'virtual_size')} +registry_references_mapping = {'keyValueCreated': 'creates', 'keyValueModified': 'modifies'} +regkey_object_mapping = {'name': ('text', 'name'), 'newdata': ('text', 'data'), + 'path': ('regkey', 'key')} +signerinfo_object_mapping = {'sigissuer': ('text', 'issuer'), + 'version': ('text', 'version')} + + +class JoeParser(): + def __init__(self, data): + self.data = data + self.misp_event = MISPEvent() + self.references = defaultdict(list) + self.attributes = defaultdict(lambda: defaultdict(set)) + self.process_references = {} + + def parse_joe(self): + if self.analysis_type() == "file": + self.parse_fileinfo() + else: + self.parse_url_analysis() + + self.parse_system_behavior() + self.parse_network_behavior() + self.parse_network_interactions() + self.parse_dropped_files() + + if self.attributes: + self.handle_attributes() + if self.references: + self.build_references() + self.parse_mitre_attack() + self.finalize_results() + + def build_references(self): + for misp_object in self.misp_event.objects: + object_uuid = misp_object.uuid + if object_uuid in self.references: + for reference in self.references[object_uuid]: + misp_object.add_reference(reference['idref'], reference['relationship']) + + def handle_attributes(self): + for attribute_type, attribute in self.attributes.items(): + for attribute_value, references in attribute.items(): + attribute_uuid = self.create_attribute(attribute_type, attribute_value) + for reference in references: + source_uuid, relationship = reference + self.references[source_uuid].append({'idref': attribute_uuid, 'relationship': relationship}) + + def parse_dropped_files(self): + droppedinfo = self.data['droppedinfo'] + if droppedinfo: + for droppedfile in droppedinfo['hash']: + file_object = MISPObject('file') + for key, mapping in dropped_file_mapping.items(): + attribute_type, object_relation = mapping + file_object.add_attribute(object_relation, **{'type': attribute_type, 'value': droppedfile[key]}) + if droppedfile['@malicious'] == 'true': + file_object.add_attribute('state', **{'type': 'text', 'value': 'Malicious'}) + for h in droppedfile['value']: + hash_type = dropped_hash_mapping[h['@algo']] + file_object.add_attribute(hash_type, **{'type': hash_type, 'value': h['$']}) + self.misp_event.add_object(**file_object) + self.references[self.process_references[(int(droppedfile['@targetid']), droppedfile['@process'])]].append({ + 'idref': file_object.uuid, + 'relationship': 'drops' + }) + + def parse_mitre_attack(self): + mitreattack = self.data['mitreattack'] + if mitreattack: + for tactic in mitreattack['tactic']: + if tactic.get('technique'): + for technique in tactic['technique']: + self.misp_event.add_tag('misp-galaxy:mitre-attack-pattern="{} - {}"'.format(technique['name'], technique['id'])) + + def parse_network_behavior(self): + network = self.data['behavior']['network'] + connections = defaultdict(lambda: defaultdict(set)) + for protocol, layer in protocols.items(): + if network.get(protocol): + for packet in network[protocol]['packet']: + timestamp = datetime.strptime(self.parse_timestamp(packet['timestamp']), '%B %d, %Y %H:%M:%S.%f') + connections[tuple(packet[field] for field in network_behavior_fields)][protocol].add(timestamp) + for connection, data in connections.items(): + attributes = self.prefetch_attributes_data(connection) + if len(data.keys()) == len(set(protocols[protocol] for protocol in data.keys())): + network_connection_object = MISPObject('network-connection') + for object_relation, attribute in attributes.items(): + network_connection_object.add_attribute(object_relation, **attribute) + network_connection_object.add_attribute('first-packet-seen', + **{'type': 'datetime', 'value': min(tuple(min(timestamp) for timestamp in data.values()))}) + for protocol in data.keys(): + network_connection_object.add_attribute('layer{}-protocol'.format(protocols[protocol]), **{'type': 'text', 'value': protocol}) + self.misp_event.add_object(**network_connection_object) + self.references[self.analysisinfo_uuid].append({'idref': network_connection_object.uuid, 'relationship': 'initiates'}) + else: + for protocol, timestamps in data.items(): + network_connection_object = MISPObject('network-connection') + for object_relation, attribute in attributes.items(): + network_connection_object.add_attribute(object_relation, **attribute) + network_connection_object.add_attribute('first-packet-seen', **{'type': 'datetime', 'value': min(timestamps)}) + network_connection_object.add_attribute('layer{}-protocol'.format(protocols[protocol]), **{'type': 'text', 'value': protocol}) + self.misp_event.add_object(**network_connection_object) + self.references[self.analysisinfo_uuid].append({'idref': network_connection_object.uuid, 'relationship': 'initiates'}) + + def parse_system_behavior(self): + system = self.data['behavior']['system'] + if system.get('processes'): + process_activities = {'fileactivities': self.parse_fileactivities, + 'registryactivities': self.parse_registryactivities} + for process in system['processes']['process']: + general = process['general'] + process_object = MISPObject('process') + for feature, relation in process_object_fields.items(): + process_object.add_attribute(relation, **{'type': 'text', 'value': general[feature]}) + start_time = datetime.strptime('{} {}'.format(general['date'], general['time']), '%d/%m/%Y %H:%M:%S') + process_object.add_attribute('start-time', **{'type': 'datetime', 'value': start_time}) + self.misp_event.add_object(**process_object) + for field, to_call in process_activities.items(): + to_call(process_object.uuid, process[field]) + self.references[self.analysisinfo_uuid].append({'idref': process_object.uuid, 'relationship': 'calls'}) + self.process_references[(general['targetid'], general['path'])] = process_object.uuid + + def parse_fileactivities(self, process_uuid, fileactivities): + for feature, files in fileactivities.items(): + # ignore unknown features + if feature not in file_references_mapping: + continue + + if files: + for call in files['call']: + self.attributes['filename'][call['path']].add((process_uuid, file_references_mapping[feature])) + + def analysis_type(self): + generalinfo = self.data['generalinfo'] + + if generalinfo['target']['sample']: + return "file" + elif generalinfo['target']['url']: + return "url" + else: + raise Exception("Unknown analysis type") + + def parse_url_analysis(self): + generalinfo = self.data["generalinfo"] + + url_object = MISPObject("url") + self.analysisinfo_uuid = url_object.uuid + + url_object.add_attribute("url", generalinfo["target"]["url"]) + self.misp_event.add_object(**url_object) + + def parse_fileinfo(self): + fileinfo = self.data['fileinfo'] + + file_object = MISPObject('file') + self.analysisinfo_uuid = file_object.uuid + + for field in file_object_fields: + file_object.add_attribute(field, **{'type': field, 'value': fileinfo[field]}) + for field, mapping in file_object_mapping.items(): + attribute_type, object_relation = mapping + file_object.add_attribute(object_relation, **{'type': attribute_type, 'value': fileinfo[field]}) + if not fileinfo.get('pe'): + self.misp_event.add_object(**file_object) + return + peinfo = fileinfo['pe'] + pe_object = MISPObject('pe') + file_object.add_reference(pe_object.uuid, 'included-in') + self.misp_event.add_object(**file_object) + for field, mapping in pe_object_fields.items(): + attribute_type, object_relation = mapping + pe_object.add_attribute(object_relation, **{'type': attribute_type, 'value': peinfo[field]}) + pe_object.add_attribute('compilation-timestamp', **{'type': 'datetime', 'value': int(peinfo['timestamp'].split()[0], 16)}) + program_name = fileinfo['filename'] + if peinfo['versions']: + for feature in peinfo['versions']['version']: + name = feature['name'] + if name == 'InternalName': + program_name = feature['value'] + if name in pe_object_mapping: + pe_object.add_attribute(pe_object_mapping[name], **{'type': 'text', 'value': feature['value']}) + sections_number = len(peinfo['sections']['section']) + pe_object.add_attribute('number-sections', **{'type': 'counter', 'value': sections_number}) + signatureinfo = peinfo['signature'] + if signatureinfo['signed']: + signerinfo_object = MISPObject('authenticode-signerinfo') + pe_object.add_reference(signerinfo_object.uuid, 'signed-by') + self.misp_event.add_object(**pe_object) + signerinfo_object.add_attribute('program-name', **{'type': 'text', 'value': program_name}) + for feature, mapping in signerinfo_object_mapping.items(): + attribute_type, object_relation = mapping + signerinfo_object.add_attribute(object_relation, **{'type': attribute_type, 'value': signatureinfo[feature]}) + self.misp_event.add_object(**signerinfo_object) + else: + self.misp_event.add_object(**pe_object) + for section in peinfo['sections']['section']: + section_object = self.parse_pe_section(section) + self.references[pe_object.uuid].append({'idref': section_object.uuid, 'relationship': 'included-in'}) + self.misp_event.add_object(**section_object) + + def parse_network_interactions(self): + domaininfo = self.data['domaininfo'] + if domaininfo: + for domain in domaininfo['domain']: + domain_object = MISPObject('domain-ip') + for key, mapping in domain_object_mapping.items(): + attribute_type, object_relation = mapping + domain_object.add_attribute(object_relation, **{'type': attribute_type, 'value': domain[key]}) + self.misp_event.add_object(**domain_object) + self.references[self.process_references[(int(domain['@targetid']), domain['@currentpath'])]].append({ + 'idref': domain_object.uuid, + 'relationship': 'contacts' + }) + ipinfo = self.data['ipinfo'] + if ipinfo: + for ip in ipinfo['ip']: + attribute = MISPAttribute() + attribute.from_dict(**{'type': 'ip-dst', 'value': ip['@ip']}) + self.misp_event.add_attribute(**attribute) + self.references[self.process_references[(int(ip['@targetid']), ip['@currentpath'])]].append({ + 'idref': attribute.uuid, + 'relationship': 'contacts' + }) + urlinfo = self.data['urlinfo'] + if urlinfo: + for url in urlinfo['url']: + target_id = int(url['@targetid']) + current_path = url['@currentpath'] + attribute = MISPAttribute() + attribute_dict = {'type': 'url', 'value': url['@name']} + if target_id != -1 and current_path != 'unknown': + self.references[self.process_references[(target_id, current_path)]].append({ + 'idref': attribute.uuid, + 'relationship': 'contacts' + }) + else: + attribute_dict['comment'] = 'From Memory - Enriched via the joe_import module' + attribute.from_dict(**attribute_dict) + self.misp_event.add_attribute(**attribute) + + def parse_pe_section(self, section): + section_object = MISPObject('pe-section') + for feature, mapping in section_object_mapping.items(): + attribute_type, object_relation = mapping + section_object.add_attribute(object_relation, **{'type': attribute_type, 'value': section[feature]}) + return section_object + + def parse_registryactivities(self, process_uuid, registryactivities): + if registryactivities['keyCreated']: + for call in registryactivities['keyCreated']['call']: + self.attributes['regkey'][call['path']].add((process_uuid, 'creates')) + for feature, relationship_type in registry_references_mapping.items(): + if registryactivities[feature]: + for call in registryactivities[feature]['call']: + registry_key = MISPObject('registry-key') + for field, mapping in regkey_object_mapping.items(): + attribute_type, object_relation = mapping + registry_key.add_attribute(object_relation, **{'type': attribute_type, 'value': call[field]}) + registry_key.add_attribute('data-type', **{'type': 'text', 'value': 'REG_{}'.format(call['type'].upper())}) + self.misp_event.add_object(**registry_key) + self.references[process_uuid].append({'idref': registry_key.uuid, 'relationship': relationship_type}) + + def create_attribute(self, attribute_type, attribute_value): + attribute = MISPAttribute() + attribute.from_dict(**{'type': attribute_type, 'value': attribute_value}) + self.misp_event.add_attribute(**attribute) + return attribute.uuid + + def finalize_results(self): + event = json.loads(self.misp_event.to_json())['Event'] + self.results = {key: event[key] for key in ('Attribute', 'Object') if (key in event and event[key])} + + @staticmethod + def parse_timestamp(timestamp): + timestamp = timestamp.split(':') + timestamp[-1] = str(round(float(timestamp[-1].split(' ')[0]), 6)) + return ':'.join(timestamp) + + @staticmethod + def prefetch_attributes_data(connection): + attributes = {} + for field, value in zip(network_behavior_fields, connection): + attribute_type, object_relation = network_connection_object_mapping[field] + attributes[object_relation] = {'type': attribute_type, 'value': value} + return attributes diff --git a/misp_modules/modules/import_mod/joe_import.py b/misp_modules/modules/import_mod/joe_import.py index 2dc8990..c1300c4 100644 --- a/misp_modules/modules/import_mod/joe_import.py +++ b/misp_modules/modules/import_mod/joe_import.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- -from collections import defaultdict -from datetime import datetime -from pymisp import MISPAttribute, MISPEvent, MISPObject -import json import base64 +import json +import os +import sys +sys.path.append('{}/lib'.format('/'.join((os.path.realpath(__file__)).split('/')[:-3]))) +from joe_parser import JoeParser misperrors = {'error': 'Error'} userConfig = {} @@ -15,326 +16,6 @@ moduleinfo = {'version': '0.1', 'author': 'Christian Studer', moduleconfig = [] -domain_object_mapping = {'@ip': ('ip-dst', 'ip'), '@name': ('domain', 'domain')} -dropped_file_mapping = {'@entropy': ('float', 'entropy'), - '@file': ('filename', 'filename'), - '@size': ('size-in-bytes', 'size-in-bytes'), - '@type': ('mime-type', 'mimetype')} -dropped_hash_mapping = {'MD5': 'md5', 'SHA': 'sha1', 'SHA-256': 'sha256', 'SHA-512': 'sha512'} -file_object_fields = ['filename', 'md5', 'sha1', 'sha256', 'sha512', 'ssdeep'] -file_object_mapping = {'entropy': ('float', 'entropy'), - 'filesize': ('size-in-bytes', 'size-in-bytes'), - 'filetype': ('mime-type', 'mimetype')} -file_references_mapping = {'fileCreated': 'creates', 'fileDeleted': 'deletes', - 'fileMoved': 'moves', 'fileRead': 'reads', 'fileWritten': 'writes'} -network_behavior_fields = ('srcip', 'dstip', 'srcport', 'dstport') -network_connection_object_mapping = {'srcip': ('ip-src', 'ip-src'), 'dstip': ('ip-dst', 'ip-dst'), - 'srcport': ('port', 'src-port'), 'dstport': ('port', 'dst-port')} -pe_object_fields = {'entrypoint': ('text', 'entrypoint-address'), - 'imphash': ('imphash', 'imphash')} -pe_object_mapping = {'CompanyName': 'company-name', 'FileDescription': 'file-description', - 'FileVersion': 'file-version', 'InternalName': 'internal-filename', - 'LegalCopyright': 'legal-copyright', 'OriginalFilename': 'original-filename', - 'ProductName': 'product-filename', 'ProductVersion': 'product-version', - 'Translation': 'lang-id'} -process_object_fields = {'cmdline': 'command-line', 'name': 'name', - 'parentpid': 'parent-pid', 'pid': 'pid', - 'path': 'current-directory'} -protocols = {'tcp': 4, 'udp': 4, 'icmp': 3, - 'http': 7, 'https': 7, 'ftp': 7} -section_object_mapping = {'characteristics': ('text', 'characteristic'), - 'entropy': ('float', 'entropy'), - 'name': ('text', 'name'), 'rawaddr': ('hex', 'offset'), - 'rawsize': ('size-in-bytes', 'size-in-bytes'), - 'virtaddr': ('hex', 'virtual_address'), - 'virtsize': ('size-in-bytes', 'virtual_size')} -registry_references_mapping = {'keyValueCreated': 'creates', 'keyValueModified': 'modifies'} -regkey_object_mapping = {'name': ('text', 'name'), 'newdata': ('text', 'data'), - 'path': ('regkey', 'key')} -signerinfo_object_mapping = {'sigissuer': ('text', 'issuer'), - 'version': ('text', 'version')} - - -class JoeParser(): - def __init__(self, data): - self.data = data - self.misp_event = MISPEvent() - self.references = defaultdict(list) - self.attributes = defaultdict(lambda: defaultdict(set)) - self.process_references = {} - - def parse_joe(self): - if self.analysis_type() == "file": - self.parse_fileinfo() - else: - self.parse_url_analysis() - - self.parse_system_behavior() - self.parse_network_behavior() - self.parse_network_interactions() - self.parse_dropped_files() - - if self.attributes: - self.handle_attributes() - if self.references: - self.build_references() - self.parse_mitre_attack() - self.finalize_results() - - def build_references(self): - for misp_object in self.misp_event.objects: - object_uuid = misp_object.uuid - if object_uuid in self.references: - for reference in self.references[object_uuid]: - misp_object.add_reference(reference['idref'], reference['relationship']) - - def handle_attributes(self): - for attribute_type, attribute in self.attributes.items(): - for attribute_value, references in attribute.items(): - attribute_uuid = self.create_attribute(attribute_type, attribute_value) - for reference in references: - source_uuid, relationship = reference - self.references[source_uuid].append({'idref': attribute_uuid, 'relationship': relationship}) - - def parse_dropped_files(self): - droppedinfo = self.data['droppedinfo'] - if droppedinfo: - for droppedfile in droppedinfo['hash']: - file_object = MISPObject('file') - for key, mapping in dropped_file_mapping.items(): - attribute_type, object_relation = mapping - file_object.add_attribute(object_relation, **{'type': attribute_type, 'value': droppedfile[key]}) - if droppedfile['@malicious'] == 'true': - file_object.add_attribute('state', **{'type': 'text', 'value': 'Malicious'}) - for h in droppedfile['value']: - hash_type = dropped_hash_mapping[h['@algo']] - file_object.add_attribute(hash_type, **{'type': hash_type, 'value': h['$']}) - self.misp_event.add_object(**file_object) - self.references[self.process_references[(int(droppedfile['@targetid']), droppedfile['@process'])]].append({ - 'idref': file_object.uuid, - 'relationship': 'drops' - }) - - def parse_mitre_attack(self): - mitreattack = self.data['mitreattack'] - if mitreattack: - for tactic in mitreattack['tactic']: - if tactic.get('technique'): - for technique in tactic['technique']: - self.misp_event.add_tag('misp-galaxy:mitre-attack-pattern="{} - {}"'.format(technique['name'], technique['id'])) - - def parse_network_behavior(self): - network = self.data['behavior']['network'] - connections = defaultdict(lambda: defaultdict(set)) - for protocol, layer in protocols.items(): - if network.get(protocol): - for packet in network[protocol]['packet']: - timestamp = datetime.strptime(self.parse_timestamp(packet['timestamp']), '%B %d, %Y %H:%M:%S.%f') - connections[tuple(packet[field] for field in network_behavior_fields)][protocol].add(timestamp) - for connection, data in connections.items(): - attributes = self.prefetch_attributes_data(connection) - if len(data.keys()) == len(set(protocols[protocol] for protocol in data.keys())): - network_connection_object = MISPObject('network-connection') - for object_relation, attribute in attributes.items(): - network_connection_object.add_attribute(object_relation, **attribute) - network_connection_object.add_attribute('first-packet-seen', - **{'type': 'datetime', 'value': min(tuple(min(timestamp) for timestamp in data.values()))}) - for protocol in data.keys(): - network_connection_object.add_attribute('layer{}-protocol'.format(protocols[protocol]), **{'type': 'text', 'value': protocol}) - self.misp_event.add_object(**network_connection_object) - self.references[self.analysisinfo_uuid].append({'idref': network_connection_object.uuid, 'relationship': 'initiates'}) - else: - for protocol, timestamps in data.items(): - network_connection_object = MISPObject('network-connection') - for object_relation, attribute in attributes.items(): - network_connection_object.add_attribute(object_relation, **attribute) - network_connection_object.add_attribute('first-packet-seen', **{'type': 'datetime', 'value': min(timestamps)}) - network_connection_object.add_attribute('layer{}-protocol'.format(protocols[protocol]), **{'type': 'text', 'value': protocol}) - self.misp_event.add_object(**network_connection_object) - self.references[self.analysisinfo_uuid].append({'idref': network_connection_object.uuid, 'relationship': 'initiates'}) - - def parse_system_behavior(self): - system = self.data['behavior']['system'] - if system.get('processes'): - process_activities = {'fileactivities': self.parse_fileactivities, - 'registryactivities': self.parse_registryactivities} - for process in system['processes']['process']: - general = process['general'] - process_object = MISPObject('process') - for feature, relation in process_object_fields.items(): - process_object.add_attribute(relation, **{'type': 'text', 'value': general[feature]}) - start_time = datetime.strptime('{} {}'.format(general['date'], general['time']), '%d/%m/%Y %H:%M:%S') - process_object.add_attribute('start-time', **{'type': 'datetime', 'value': start_time}) - self.misp_event.add_object(**process_object) - for field, to_call in process_activities.items(): - to_call(process_object.uuid, process[field]) - self.references[self.analysisinfo_uuid].append({'idref': process_object.uuid, 'relationship': 'calls'}) - self.process_references[(general['targetid'], general['path'])] = process_object.uuid - - def parse_fileactivities(self, process_uuid, fileactivities): - for feature, files in fileactivities.items(): - # ignore unknown features - if feature not in file_references_mapping: - continue - - if files: - for call in files['call']: - self.attributes['filename'][call['path']].add((process_uuid, file_references_mapping[feature])) - - def analysis_type(self): - generalinfo = self.data['generalinfo'] - - if generalinfo['target']['sample']: - return "file" - elif generalinfo['target']['url']: - return "url" - else: - raise Exception("Unknown analysis type") - - def parse_url_analysis(self): - generalinfo = self.data["generalinfo"] - - url_object = MISPObject("url") - self.analysisinfo_uuid = url_object.uuid - - url_object.add_attribute("url", generalinfo["target"]["url"]) - self.misp_event.add_object(**url_object) - - def parse_fileinfo(self): - fileinfo = self.data['fileinfo'] - - file_object = MISPObject('file') - self.analysisinfo_uuid = file_object.uuid - - for field in file_object_fields: - file_object.add_attribute(field, **{'type': field, 'value': fileinfo[field]}) - for field, mapping in file_object_mapping.items(): - attribute_type, object_relation = mapping - file_object.add_attribute(object_relation, **{'type': attribute_type, 'value': fileinfo[field]}) - if not fileinfo.get('pe'): - self.misp_event.add_object(**file_object) - return - peinfo = fileinfo['pe'] - pe_object = MISPObject('pe') - file_object.add_reference(pe_object.uuid, 'included-in') - self.misp_event.add_object(**file_object) - for field, mapping in pe_object_fields.items(): - attribute_type, object_relation = mapping - pe_object.add_attribute(object_relation, **{'type': attribute_type, 'value': peinfo[field]}) - pe_object.add_attribute('compilation-timestamp', **{'type': 'datetime', 'value': int(peinfo['timestamp'].split()[0], 16)}) - program_name = fileinfo['filename'] - if peinfo['versions']: - for feature in peinfo['versions']['version']: - name = feature['name'] - if name == 'InternalName': - program_name = feature['value'] - if name in pe_object_mapping: - pe_object.add_attribute(pe_object_mapping[name], **{'type': 'text', 'value': feature['value']}) - sections_number = len(peinfo['sections']['section']) - pe_object.add_attribute('number-sections', **{'type': 'counter', 'value': sections_number}) - signatureinfo = peinfo['signature'] - if signatureinfo['signed']: - signerinfo_object = MISPObject('authenticode-signerinfo') - pe_object.add_reference(signerinfo_object.uuid, 'signed-by') - self.misp_event.add_object(**pe_object) - signerinfo_object.add_attribute('program-name', **{'type': 'text', 'value': program_name}) - for feature, mapping in signerinfo_object_mapping.items(): - attribute_type, object_relation = mapping - signerinfo_object.add_attribute(object_relation, **{'type': attribute_type, 'value': signatureinfo[feature]}) - self.misp_event.add_object(**signerinfo_object) - else: - self.misp_event.add_object(**pe_object) - for section in peinfo['sections']['section']: - section_object = self.parse_pe_section(section) - self.references[pe_object.uuid].append({'idref': section_object.uuid, 'relationship': 'included-in'}) - self.misp_event.add_object(**section_object) - - def parse_network_interactions(self): - domaininfo = self.data['domaininfo'] - if domaininfo: - for domain in domaininfo['domain']: - domain_object = MISPObject('domain-ip') - for key, mapping in domain_object_mapping.items(): - attribute_type, object_relation = mapping - domain_object.add_attribute(object_relation, **{'type': attribute_type, 'value': domain[key]}) - self.misp_event.add_object(**domain_object) - self.references[self.process_references[(int(domain['@targetid']), domain['@currentpath'])]].append({ - 'idref': domain_object.uuid, - 'relationship': 'contacts' - }) - ipinfo = self.data['ipinfo'] - if ipinfo: - for ip in ipinfo['ip']: - attribute = MISPAttribute() - attribute.from_dict(**{'type': 'ip-dst', 'value': ip['@ip']}) - self.misp_event.add_attribute(**attribute) - self.references[self.process_references[(int(ip['@targetid']), ip['@currentpath'])]].append({ - 'idref': attribute.uuid, - 'relationship': 'contacts' - }) - urlinfo = self.data['urlinfo'] - if urlinfo: - for url in urlinfo['url']: - target_id = int(url['@targetid']) - current_path = url['@currentpath'] - attribute = MISPAttribute() - attribute_dict = {'type': 'url', 'value': url['@name']} - if target_id != -1 and current_path != 'unknown': - self.references[self.process_references[(target_id, current_path)]].append({ - 'idref': attribute.uuid, - 'relationship': 'contacts' - }) - else: - attribute_dict['comment'] = 'From Memory - Enriched via the joe_import module' - attribute.from_dict(**attribute_dict) - self.misp_event.add_attribute(**attribute) - - def parse_pe_section(self, section): - section_object = MISPObject('pe-section') - for feature, mapping in section_object_mapping.items(): - attribute_type, object_relation = mapping - section_object.add_attribute(object_relation, **{'type': attribute_type, 'value': section[feature]}) - return section_object - - def parse_registryactivities(self, process_uuid, registryactivities): - if registryactivities['keyCreated']: - for call in registryactivities['keyCreated']['call']: - self.attributes['regkey'][call['path']].add((process_uuid, 'creates')) - for feature, relationship_type in registry_references_mapping.items(): - if registryactivities[feature]: - for call in registryactivities[feature]['call']: - registry_key = MISPObject('registry-key') - for field, mapping in regkey_object_mapping.items(): - attribute_type, object_relation = mapping - registry_key.add_attribute(object_relation, **{'type': attribute_type, 'value': call[field]}) - registry_key.add_attribute('data-type', **{'type': 'text', 'value': 'REG_{}'.format(call['type'].upper())}) - self.misp_event.add_object(**registry_key) - self.references[process_uuid].append({'idref': registry_key.uuid, 'relationship': relationship_type}) - - def create_attribute(self, attribute_type, attribute_value): - attribute = MISPAttribute() - attribute.from_dict(**{'type': attribute_type, 'value': attribute_value}) - self.misp_event.add_attribute(**attribute) - return attribute.uuid - - def finalize_results(self): - event = json.loads(self.misp_event.to_json())['Event'] - self.results = {key: event[key] for key in ('Attribute', 'Object') if (key in event and event[key])} - - @staticmethod - def parse_timestamp(timestamp): - timestamp = timestamp.split(':') - timestamp[-1] = str(round(float(timestamp[-1].split(' ')[0]), 6)) - return ':'.join(timestamp) - - @staticmethod - def prefetch_attributes_data(connection): - attributes = {} - for field, value in zip(network_behavior_fields, connection): - attribute_type, object_relation = network_connection_object_mapping[field] - attributes[object_relation] = {'type': attribute_type, 'value': value} - return attributes - def handler(q=False): if q is False: From 0d40830a7f566dec9de1423edc24af079a205608 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 3 Jun 2019 18:35:58 +1000 Subject: [PATCH 151/207] fix: Some quick fixes - Fixed strptime matching because months are expressed in abbreviated format - Made data loaded while the parsing function is called, in case it has to be called multiple times at some point --- misp_modules/lib/joe_parser.py | 12 ++++++------ misp_modules/modules/import_mod/joe_import.py | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/misp_modules/lib/joe_parser.py b/misp_modules/lib/joe_parser.py index a3dc82c..5af78f2 100644 --- a/misp_modules/lib/joe_parser.py +++ b/misp_modules/lib/joe_parser.py @@ -46,14 +46,14 @@ signerinfo_object_mapping = {'sigissuer': ('text', 'issuer'), class JoeParser(): - def __init__(self, data): - self.data = data + def __init__(self): self.misp_event = MISPEvent() self.references = defaultdict(list) self.attributes = defaultdict(lambda: defaultdict(set)) self.process_references = {} - def parse_joe(self): + def parse_data(self, data): + self.data = data if self.analysis_type() == "file": self.parse_fileinfo() else: @@ -66,8 +66,6 @@ class JoeParser(): if self.attributes: self.handle_attributes() - if self.references: - self.build_references() self.parse_mitre_attack() self.finalize_results() @@ -119,7 +117,7 @@ class JoeParser(): for protocol, layer in protocols.items(): if network.get(protocol): for packet in network[protocol]['packet']: - timestamp = datetime.strptime(self.parse_timestamp(packet['timestamp']), '%B %d, %Y %H:%M:%S.%f') + timestamp = datetime.strptime(self.parse_timestamp(packet['timestamp']), '%b %d, %Y %H:%M:%S.%f') connections[tuple(packet[field] for field in network_behavior_fields)][protocol].add(timestamp) for connection, data in connections.items(): attributes = self.prefetch_attributes_data(connection) @@ -308,6 +306,8 @@ class JoeParser(): return attribute.uuid def finalize_results(self): + if self.references: + self.build_references() event = json.loads(self.misp_event.to_json())['Event'] self.results = {key: event[key] for key in ('Attribute', 'Object') if (key in event and event[key])} diff --git a/misp_modules/modules/import_mod/joe_import.py b/misp_modules/modules/import_mod/joe_import.py index c1300c4..d4b9dfb 100644 --- a/misp_modules/modules/import_mod/joe_import.py +++ b/misp_modules/modules/import_mod/joe_import.py @@ -24,9 +24,9 @@ def handler(q=False): data = base64.b64decode(q.get('data')).decode('utf-8') if not data: return json.dumps({'success': 0}) - joe_data = json.loads(data)['analysis'] - joe_parser = JoeParser(joe_data) - joe_parser.parse_joe() + joe_parser = JoeParser() + joe_parser.parse_data(json.loads(data)['analysis']) + joe_parser.finalize_results() return {'results': joe_parser.results} From 07698e5c72ef28c7d9b7eb91989e5c994b3092a4 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 3 Jun 2019 18:38:58 +1000 Subject: [PATCH 152/207] fix: Fixed references between domaininfo/ipinfo & their targets - Fixed references when no target id is set - Fixed domaininfo parsing when no ip is defined --- misp_modules/lib/joe_parser.py | 35 +++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/misp_modules/lib/joe_parser.py b/misp_modules/lib/joe_parser.py index 5af78f2..bf5c974 100644 --- a/misp_modules/lib/joe_parser.py +++ b/misp_modules/lib/joe_parser.py @@ -241,25 +241,28 @@ class JoeParser(): domaininfo = self.data['domaininfo'] if domaininfo: for domain in domaininfo['domain']: - domain_object = MISPObject('domain-ip') - for key, mapping in domain_object_mapping.items(): - attribute_type, object_relation = mapping - domain_object.add_attribute(object_relation, **{'type': attribute_type, 'value': domain[key]}) - self.misp_event.add_object(**domain_object) - self.references[self.process_references[(int(domain['@targetid']), domain['@currentpath'])]].append({ - 'idref': domain_object.uuid, - 'relationship': 'contacts' - }) + if domain['@ip'] != 'unknown': + domain_object = MISPObject('domain-ip') + for key, mapping in domain_object_mapping.items(): + attribute_type, object_relation = mapping + domain_object.add_attribute(object_relation, + **{'type': attribute_type, 'value': domain[key]}) + self.misp_event.add_object(**domain_object) + reference = {'idref': domain_object.uuid, 'relationship': 'contacts'} + self.add_process_reference(domain['@targetid'], domain['@currentpath'], reference) + else: + attribute = MISPAttribute() + attribute.from_dict(**{'type': 'domain', 'value': domain['@name']}) + reference = {'idref': attribute.uuid, 'relationship': 'contacts'} + self.add_process_reference(domain['@targetid'], domain['@currentpath'], reference) ipinfo = self.data['ipinfo'] if ipinfo: for ip in ipinfo['ip']: attribute = MISPAttribute() attribute.from_dict(**{'type': 'ip-dst', 'value': ip['@ip']}) self.misp_event.add_attribute(**attribute) - self.references[self.process_references[(int(ip['@targetid']), ip['@currentpath'])]].append({ - 'idref': attribute.uuid, - 'relationship': 'contacts' - }) + reference = {'idref': attribute.uuid, 'relationship': 'contacts'} + self.add_process_reference(ip['@targetid'], ip['@currentpath'], reference) urlinfo = self.data['urlinfo'] if urlinfo: for url in urlinfo['url']: @@ -299,6 +302,12 @@ class JoeParser(): self.misp_event.add_object(**registry_key) self.references[process_uuid].append({'idref': registry_key.uuid, 'relationship': relationship_type}) + def add_process_reference(self, target, currentpath, reference): + try: + self.references[self.process_references[(int(target), currentpath)]].append(reference) + except KeyError: + self.references[self.analysisinfo_uuid].append(reference) + def create_attribute(self, attribute_type, attribute_value): attribute = MISPAttribute() attribute.from_dict(**{'type': attribute_type, 'value': attribute_value}) From ee48d9984522da86c5a76fdc878f2fb0c1f7bccc Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Tue, 4 Jun 2019 09:48:50 +1000 Subject: [PATCH 153/207] add: New expansion module to query Joe Sandbox API with a report link --- misp_modules/modules/expansion/__init__.py | 2 +- .../modules/expansion/joesandbox_query.py | 63 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 misp_modules/modules/expansion/joesandbox_query.py diff --git a/misp_modules/modules/expansion/__init__.py b/misp_modules/modules/expansion/__init__.py index ba10b32..f64600a 100644 --- a/misp_modules/modules/expansion/__init__.py +++ b/misp_modules/modules/expansion/__init__.py @@ -10,4 +10,4 @@ __all__ = ['cuckoo_submit', 'vmray_submit', 'bgpranking', 'circl_passivedns', 'c 'sigma_queries', 'dbl_spamhaus', 'vulners', 'yara_query', 'macaddress_io', 'intel471', 'backscatter_io', 'btc_scam_check', 'hibp', 'greynoise', 'macvendors', 'qrcode', 'ocr-enrich', 'pdf-enrich', 'docx-enrich', 'xlsx-enrich', 'pptx-enrich', - 'ods-enrich', 'odt-enrich', 'joesandbox_submit', 'urlhaus'] + 'ods-enrich', 'odt-enrich', 'joesandbox_submit', 'joesandbox_query', 'urlhaus'] diff --git a/misp_modules/modules/expansion/joesandbox_query.py b/misp_modules/modules/expansion/joesandbox_query.py new file mode 100644 index 0000000..ea58cb6 --- /dev/null +++ b/misp_modules/modules/expansion/joesandbox_query.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +import jbxapi +import json +import os +import sys +sys.path.append('{}/lib'.format('/'.join((os.path.realpath(__file__)).split('/')[:-3]))) +from joe_parser import JoeParser + +misperrors = {'error': 'Error'} +mispattributes = {'input': ['link'], 'format': 'misp_standard'} + +moduleinfo = {'version': '0.1', 'author': 'Christian Studer', + 'description': 'Query Joe Sandbox API with a report URL to get the parsed data.', + 'module-type': ['expansion']} +moduleconfig = ['apiurl', 'apikey', 'accept-tac'] + + +class _ParseError(Exception): + pass + + +def _parse_bool(value, name="bool"): + if value is None or value == "": + return None + if value in ("true", "True"): + return True + if value in ("false", "False"): + return False + raise _ParseError("Cannot parse {}. Must be 'true' or 'false'".format(name)) + + +def handler(q=False): + if q is False: + return False + request = json.loads(q) + apiurl = request['config'].get('apiurl') or 'https://jbxcloud.joesecurity.org/api' + apikey = request['config'].get('apikey') + if not apikey: + return {'error': 'No API key provided'} + try: + accept_tac = _parse_bool(request['config'].get('accept-tac'), 'accept-tac') + except _parseError as e: + return {'error': str(e)} + attribute = request['attribute'] + joe = jbxapi.JoeSandbox(apiurl=apiurl, apikey=apikey, user_agent='MISP joesandbox_analysis', accept_tac=accept_tac) + joe_info = joe.submission_info(attribute['value'].split('/')[-1]) + joe_parser = JoeParser() + most_relevant = joe_info['most_relevant_analysis']['webid'] + for analyse in joe_info['analyses']: + if analyse['webid'] == most_relevant: + joe_data = json.loads(joe.analysis_download(most_relevant, 'jsonfixed')[1]) + joe_parser.parse_data(joe_data['analysis']) + break + joe_parser.finalize_results() + return {'results': joe_parser.results} + + +def introspection(): + return mispattributes + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo From 42bc6f8d2b485b6aed77029b8cc2469ad151c0d4 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Tue, 4 Jun 2019 11:32:21 +1000 Subject: [PATCH 154/207] fix: Fixed variable name typo --- misp_modules/modules/expansion/joesandbox_query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/joesandbox_query.py b/misp_modules/modules/expansion/joesandbox_query.py index ea58cb6..225cc16 100644 --- a/misp_modules/modules/expansion/joesandbox_query.py +++ b/misp_modules/modules/expansion/joesandbox_query.py @@ -39,7 +39,7 @@ def handler(q=False): return {'error': 'No API key provided'} try: accept_tac = _parse_bool(request['config'].get('accept-tac'), 'accept-tac') - except _parseError as e: + except _ParseError as e: return {'error': str(e)} attribute = request['attribute'] joe = jbxapi.JoeSandbox(apiurl=apiurl, apikey=apikey, user_agent='MISP joesandbox_analysis', accept_tac=accept_tac) From aa3e87384533da48eacae7e6a1ecef8a8e776e7f Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Tue, 4 Jun 2019 11:33:42 +1000 Subject: [PATCH 155/207] fix: Making pep8 happy + added joe_import module in the init list --- misp_modules/modules/expansion/__init__.py | 3 +++ misp_modules/modules/expansion/joesandbox_query.py | 4 +--- misp_modules/modules/import_mod/__init__.py | 5 ++++- misp_modules/modules/import_mod/joe_import.py | 3 --- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/misp_modules/modules/expansion/__init__.py b/misp_modules/modules/expansion/__init__.py index f64600a..acf49f2 100644 --- a/misp_modules/modules/expansion/__init__.py +++ b/misp_modules/modules/expansion/__init__.py @@ -1,4 +1,7 @@ from . import _vmray # noqa +import os +import sys +sys.path.append('{}/lib'.format('/'.join((os.path.realpath(__file__)).split('/')[:-3]))) __all__ = ['cuckoo_submit', 'vmray_submit', 'bgpranking', 'circl_passivedns', 'circl_passivessl', 'countrycode', 'cve', 'dns', 'btc_steroids', 'domaintools', 'eupi', diff --git a/misp_modules/modules/expansion/joesandbox_query.py b/misp_modules/modules/expansion/joesandbox_query.py index 225cc16..27ced1b 100644 --- a/misp_modules/modules/expansion/joesandbox_query.py +++ b/misp_modules/modules/expansion/joesandbox_query.py @@ -1,9 +1,6 @@ # -*- coding: utf-8 -*- import jbxapi import json -import os -import sys -sys.path.append('{}/lib'.format('/'.join((os.path.realpath(__file__)).split('/')[:-3]))) from joe_parser import JoeParser misperrors = {'error': 'Error'} @@ -58,6 +55,7 @@ def handler(q=False): def introspection(): return mispattributes + def version(): moduleinfo['config'] = moduleconfig return moduleinfo diff --git a/misp_modules/modules/import_mod/__init__.py b/misp_modules/modules/import_mod/__init__.py index c3eea05..65a7069 100644 --- a/misp_modules/modules/import_mod/__init__.py +++ b/misp_modules/modules/import_mod/__init__.py @@ -1,3 +1,6 @@ from . import _vmray # noqa +import os +import sys +sys.path.append('{}/lib'.format('/'.join((os.path.realpath(__file__)).split('/')[:-3]))) -__all__ = ['vmray_import', 'ocr', 'cuckooimport', 'goamlimport', 'email_import', 'mispjson', 'openiocimport', 'threatanalyzer_import', 'csvimport'] +__all__ = ['vmray_import', 'ocr', 'cuckooimport', 'goamlimport', 'email_import', 'mispjson', 'openiocimport', 'threatanalyzer_import', 'csvimport', 'joe_import'] diff --git a/misp_modules/modules/import_mod/joe_import.py b/misp_modules/modules/import_mod/joe_import.py index d4b9dfb..d1c4d19 100644 --- a/misp_modules/modules/import_mod/joe_import.py +++ b/misp_modules/modules/import_mod/joe_import.py @@ -1,9 +1,6 @@ # -*- coding: utf-8 -*- import base64 import json -import os -import sys -sys.path.append('{}/lib'.format('/'.join((os.path.realpath(__file__)).split('/')[:-3]))) from joe_parser import JoeParser misperrors = {'error': 'Error'} From efb0a88eebd34d5320e7d06dbfa334183d776dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Georg=20Sch=C3=B6lly?= Date: Tue, 4 Jun 2019 11:29:40 +0200 Subject: [PATCH 156/207] joesandbox_query.py: improve behavior in unexpected circumstances --- .../modules/expansion/joesandbox_query.py | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/misp_modules/modules/expansion/joesandbox_query.py b/misp_modules/modules/expansion/joesandbox_query.py index 27ced1b..dce63ea 100644 --- a/misp_modules/modules/expansion/joesandbox_query.py +++ b/misp_modules/modules/expansion/joesandbox_query.py @@ -9,21 +9,7 @@ mispattributes = {'input': ['link'], 'format': 'misp_standard'} moduleinfo = {'version': '0.1', 'author': 'Christian Studer', 'description': 'Query Joe Sandbox API with a report URL to get the parsed data.', 'module-type': ['expansion']} -moduleconfig = ['apiurl', 'apikey', 'accept-tac'] - - -class _ParseError(Exception): - pass - - -def _parse_bool(value, name="bool"): - if value is None or value == "": - return None - if value in ("true", "True"): - return True - if value in ("false", "False"): - return False - raise _ParseError("Cannot parse {}. Must be 'true' or 'false'".format(name)) +moduleconfig = ['apiurl', 'apikey'] def handler(q=False): @@ -34,21 +20,32 @@ def handler(q=False): apikey = request['config'].get('apikey') if not apikey: return {'error': 'No API key provided'} + + url = request['attribute']['value'] + if "/submissions/" not in url: + return {'error': "The URL does not point to a Joe Sandbox analysis."} + + submission_id = url.split('/')[-1] # The URL has the format https://example.net/submissions/12345 + joe = jbxapi.JoeSandbox(apiurl=apiurl, apikey=apikey, user_agent='MISP joesandbox_query') + try: - accept_tac = _parse_bool(request['config'].get('accept-tac'), 'accept-tac') - except _ParseError as e: + joe_info = joe.submission_info(submission_id) + except jbxapi.ApiError as e: return {'error': str(e)} - attribute = request['attribute'] - joe = jbxapi.JoeSandbox(apiurl=apiurl, apikey=apikey, user_agent='MISP joesandbox_analysis', accept_tac=accept_tac) - joe_info = joe.submission_info(attribute['value'].split('/')[-1]) + + if joe_info["status"] != "finished": + return {'error': "The analysis has not finished yet."} + + if joe_info['most_relevant_analysis'] is None: + return {'error': "No analysis belongs to this submission."} + + analysis_webid = joe_info['most_relevant_analysis']['webid'] + joe_parser = JoeParser() - most_relevant = joe_info['most_relevant_analysis']['webid'] - for analyse in joe_info['analyses']: - if analyse['webid'] == most_relevant: - joe_data = json.loads(joe.analysis_download(most_relevant, 'jsonfixed')[1]) - joe_parser.parse_data(joe_data['analysis']) - break + joe_data = json.loads(joe.analysis_download(analysis_webid, 'jsonfixed')[1]) + joe_parser.parse_data(joe_data['analysis']) joe_parser.finalize_results() + return {'results': joe_parser.results} From b52e17fa8d670a0631d1f444b29864de28433a79 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Fri, 7 Jun 2019 11:38:50 +0200 Subject: [PATCH 157/207] fix: Removed duplicate finalize_results function call --- misp_modules/lib/joe_parser.py | 1 - 1 file changed, 1 deletion(-) diff --git a/misp_modules/lib/joe_parser.py b/misp_modules/lib/joe_parser.py index bf5c974..7ee8a4b 100644 --- a/misp_modules/lib/joe_parser.py +++ b/misp_modules/lib/joe_parser.py @@ -67,7 +67,6 @@ class JoeParser(): if self.attributes: self.handle_attributes() self.parse_mitre_attack() - self.finalize_results() def build_references(self): for misp_object in self.misp_event.objects: From de966eac5157557cb20df6a3a44ba3017290ff32 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Fri, 7 Jun 2019 15:22:11 +0200 Subject: [PATCH 158/207] fix: Returning tags & galaxies with results - Tags may exist with the current version of the parser - Galaxies are not yet expected from the parser, nevertheless the principle is we want to return them as well if ever we have some galaxies from parsing a JoeSandbox report. Can be removed if we never galaxies at all --- misp_modules/lib/joe_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/lib/joe_parser.py b/misp_modules/lib/joe_parser.py index 7ee8a4b..83f1fa0 100644 --- a/misp_modules/lib/joe_parser.py +++ b/misp_modules/lib/joe_parser.py @@ -317,7 +317,7 @@ class JoeParser(): if self.references: self.build_references() event = json.loads(self.misp_event.to_json())['Event'] - self.results = {key: event[key] for key in ('Attribute', 'Object') if (key in event and event[key])} + self.results = {key: event[key] for key in ('Attribute', 'Object', 'Tag', 'Galaxy') if (key in event and event[key])} @staticmethod def parse_timestamp(timestamp): From f885b6c5e197a084222594d4d6cdf8fb790cf8ea Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Wed, 12 Jun 2019 16:32:13 +0200 Subject: [PATCH 159/207] add: Added new modules to the list --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ecce406..bc2056a 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,8 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ * [intel471](misp_modules/modules/expansion/intel471.py) - an expansion module to get info from [Intel471](https://intel471.com). * [IPASN](misp_modules/modules/expansion/ipasn.py) - a hover and expansion to get the BGP ASN of an IP address. * [iprep](misp_modules/modules/expansion/iprep.py) - an expansion module to get IP reputation from packetmail.net. -* [Joe Sandbox](misp_modules/modules/expansion/joesandbox_submit.py) - Submit files and URLs to Joe Sandbox. +* [Joe Sandbox submit](misp_modules/modules/expansion/joesandbox_submit.py) - Submit files and URLs to Joe Sandbox. +* [Joe Sandbox query](misp_modules/modules/expansion/joesandbox_query.py) - Query Joe Sandbox with the link of an analysis and get the parsed data. * [macaddress.io](misp_modules/modules/expansion/macaddress_io.py) - a hover module to retrieve vendor details and other information regarding a given MAC address or an OUI from [MAC address Vendor Lookup](https://macaddress.io). See [integration tutorial here](https://macaddress.io/integrations/MISP-module). * [macvendors](misp_modules/modules/expansion/macvendors.py) - a hover module to retrieve mac vendor information. * [ocr-enrich](misp_modules/modules/expansion/ocr-enrich.py) - an enrichment module to get OCRized data from images into MISP. @@ -63,6 +64,7 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ * [STIX2 pattern syntax validator](misp_modules/modules/expansion/stix2_pattern_syntax_validator.py) - a module to check a STIX2 pattern syntax. * [ThreatCrowd](misp_modules/modules/expansion/threatcrowd.py) - an expansion module for [ThreatCrowd](https://www.threatcrowd.org/). * [threatminer](misp_modules/modules/expansion/threatminer.py) - an expansion module to expand from [ThreatMiner](https://www.threatminer.org/). +* [urlhaus](misp_modules/modules/expansion/urlhaus.py) - Query urlhaus to get additional data about a domain, hash, hostname, ip or url. * [urlscan](misp_modules/modules/expansion/urlscan.py) - an expansion module to query [urlscan.io](https://urlscan.io). * [virustotal](misp_modules/modules/expansion/virustotal.py) - an expansion module to pull known resolutions and malware samples related with an IP/Domain from virusTotal (this modules require a VirusTotal private API key) * [VMray](misp_modules/modules/expansion/vmray_submit.py) - a module to submit a sample to VMray. @@ -92,7 +94,8 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ * [CSV import](misp_modules/modules/import_mod/csvimport.py) Customizable CSV import module. * [Cuckoo JSON](misp_modules/modules/import_mod/cuckooimport.py) Cuckoo JSON import. * [Email Import](misp_modules/modules/import_mod/email_import.py) Email import module for MISP to import basic metadata. -* [GoAML import](misp_modules/modules/import_mod/) Module to import [GoAML](http://goaml.unodc.org/goaml/en/index.html) XML format. +* [GoAML import](misp_modules/modules/import_mod/goamlimport.py) Module to import [GoAML](http://goaml.unodc.org/goaml/en/index.html) XML format. +* [Joe Sandbox import](misp_modules/modules/import_mod/joe_import.py) Parse data from a Joe Sandbox json report. * [OCR](misp_modules/modules/import_mod/ocr.py) Optical Character Recognition (OCR) module for MISP to import attributes from images, scan or faxes. * [OpenIOC](misp_modules/modules/import_mod/openiocimport.py) OpenIOC import based on PyMISP library. * [ThreatAnalyzer](misp_modules/modules/import_mod/threatanalyzer_import.py) - An import module to process ThreatAnalyzer archive.zip/analysis.json sandbox exports. From 1ac85a4879a0e4e2d7f189e1371a9053b3125e00 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Sat, 15 Jun 2019 08:05:14 +0200 Subject: [PATCH 160/207] fix: We will display galaxies with tags --- misp_modules/lib/joe_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/lib/joe_parser.py b/misp_modules/lib/joe_parser.py index 83f1fa0..b197ee8 100644 --- a/misp_modules/lib/joe_parser.py +++ b/misp_modules/lib/joe_parser.py @@ -317,7 +317,7 @@ class JoeParser(): if self.references: self.build_references() event = json.loads(self.misp_event.to_json())['Event'] - self.results = {key: event[key] for key in ('Attribute', 'Object', 'Tag', 'Galaxy') if (key in event and event[key])} + self.results = {key: event[key] for key in ('Attribute', 'Object', 'Tag') if (key in event and event[key])} @staticmethod def parse_timestamp(timestamp): From 2f3ce1b6153540739d5db889e71f1c0bcd9a259d Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Sat, 15 Jun 2019 08:06:47 +0200 Subject: [PATCH 161/207] fix: Support of the latest version of sigmatools --- .../modules/expansion/sigma_queries.py | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/misp_modules/modules/expansion/sigma_queries.py b/misp_modules/modules/expansion/sigma_queries.py index 7799f2a..009c785 100644 --- a/misp_modules/modules/expansion/sigma_queries.py +++ b/misp_modules/modules/expansion/sigma_queries.py @@ -4,7 +4,6 @@ import json try: from sigma.parser.collection import SigmaCollectionParser from sigma.configuration import SigmaConfiguration - from sigma.backends.base import BackendOptions from sigma.backends.discovery import getBackend except ImportError: print("sigma or yaml is missing, use 'pip3 install sigmatools' to install it.") @@ -25,24 +24,20 @@ def handler(q=False): misperrors['error'] = 'Sigma rule missing' return misperrors config = SigmaConfiguration() - backend_options = BackendOptions(None) f = io.TextIOWrapper(io.BytesIO(request.get('sigma').encode()), encoding='utf-8') - parser = SigmaCollectionParser(f, config, None) + parser = SigmaCollectionParser(f, config) targets = [] - old_stdout = sys.stdout - result = io.StringIO() - sys.stdout = result + results = [] for t in sigma_targets: - backend = getBackend(t)(config, backend_options, None) + backend = getBackend(t)(config, {'rulecomment': False}) try: parser.generate(backend) - backend.finalize() - print("#NEXT") - targets.append(t) - except Exception: + result = backend.finalize() + if result: + results.append(result) + targets.append(t) + except Exception as e: continue - sys.stdout = old_stdout - results = result.getvalue()[:-5].split('#NEXT') d_result = {t: r.strip() for t, r in zip(targets, results)} return {'results': [{'types': mispattributes['output'], 'values': d_result}]} From 9fdd6c5e5822b3eb9e3d4cb96124c5e54c69c9ed Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Sat, 15 Jun 2019 08:17:29 +0200 Subject: [PATCH 162/207] fix: Making travis happy --- misp_modules/modules/expansion/sigma_queries.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/misp_modules/modules/expansion/sigma_queries.py b/misp_modules/modules/expansion/sigma_queries.py index 009c785..b7c871d 100644 --- a/misp_modules/modules/expansion/sigma_queries.py +++ b/misp_modules/modules/expansion/sigma_queries.py @@ -1,4 +1,3 @@ -import sys import io import json try: @@ -36,7 +35,7 @@ def handler(q=False): if result: results.append(result) targets.append(t) - except Exception as e: + except Exception: continue d_result = {t: r.strip() for t, r in zip(targets, results)} return {'results': [{'types': mispattributes['output'], 'values': d_result}]} From a2d58918e4286cf52687fb03b207128021b41d8b Mon Sep 17 00:00:00 2001 From: Manabu Niseki Date: Mon, 17 Jun 2019 17:50:26 +0100 Subject: [PATCH 163/207] Fix missing links in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bc2056a..aecdce3 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ ### Expansion modules -* [Backscatter.io](misp_modules/modules/expansion/backscatter_io) - a hover and expansion module to expand an IP address with mass-scanning observations. +* [Backscatter.io](misp_modules/modules/expansion/backscatter_io.py) - a hover and expansion module to expand an IP address with mass-scanning observations. * [BGP Ranking](misp_modules/modules/expansion/bgpranking.py) - a hover and expansion module to expand an AS number with the ASN description, its history, and position in BGP Ranking. * [BTC scam check](misp_modules/modules/expansion/btc_scam_check.py) - An expansion hover module to instantly check if a BTC address has been abused. * [BTC transactions](misp_modules/modules/expansion/btc_steroids.py) - An expansion hover module to get a blockchain balance and the transactions from a BTC address in MISP. @@ -70,7 +70,7 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ * [VMray](misp_modules/modules/expansion/vmray_submit.py) - a module to submit a sample to VMray. * [VulnDB](misp_modules/modules/expansion/vulndb.py) - a module to query [VulnDB](https://www.riskbasedsecurity.com/). * [Vulners](misp_modules/modules/expansion/vulners.py) - an expansion module to expand information about CVEs using Vulners API. -* [whois](misp_modules/modules/expansion) - a module to query a local instance of [uwhois](https://github.com/rafiot/uwhoisd). +* [whois](misp_modules/modules/expansion/whois.py) - a module to query a local instance of [uwhois](https://github.com/rafiot/uwhoisd). * [wikidata](misp_modules/modules/expansion/wiki.py) - a [wikidata](https://www.wikidata.org) expansion module. * [xforce](misp_modules/modules/expansion/xforceexchange.py) - an IBM X-Force Exchange expansion module. * [xlsx-enrich](misp_modules/modules/expansion/xlsx-enrich.py) - an enrichment module to get text out of an Excel document into MISP (using free-text parser). From 9e45d302b10c1730fec27251c04d2e8403102ec2 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Tue, 18 Jun 2019 09:45:59 +0200 Subject: [PATCH 164/207] fix: Testing if an object is not empty before adding it the the event --- misp_modules/modules/expansion/urlhaus.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/misp_modules/modules/expansion/urlhaus.py b/misp_modules/modules/expansion/urlhaus.py index 12893b9..64d7527 100644 --- a/misp_modules/modules/expansion/urlhaus.py +++ b/misp_modules/modules/expansion/urlhaus.py @@ -79,7 +79,8 @@ class PayloadQuery(URLhaus): file_object.add_reference(attribute.uuid, 'retrieved-from') if url[_filename_]: file_object.add_attribute(_filename_, **{'type': _filename_, 'value': url[_filename_]}) - self.misp_event.add_object(**file_object) + if any((file_object.attributes, file_object.references)): + self.misp_event.add_object(**file_object) class UrlQuery(URLhaus): @@ -106,7 +107,8 @@ class UrlQuery(URLhaus): vt_object = self._create_vt_object(payload['virustotal']) file_object.add_reference(vt_object.uuid, 'analyzed-with') self.misp_event.add_object(**vt_object) - self.misp_event.add_object(**file_object) + if any((file_object.attributes, file_object.references)): + self.misp_event.add_object(**file_object) _misp_type_mapping = {'url': UrlQuery, 'md5': PayloadQuery, 'sha256': PayloadQuery, From 7ef8acda0d969f1d50622e11d8d3cc38640ffb13 Mon Sep 17 00:00:00 2001 From: Kortho Date: Tue, 18 Jun 2019 10:31:14 +0200 Subject: [PATCH 165/207] Fixed missing dependencies for RHEL install Added dependencies needed for installing the python library pdftotext --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aecdce3..a59b98c 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ As of this writing, the official RHEL repositories only contain Ruby 2.0.0 and R ~~~~bash sudo yum install rh-ruby22 sudo yum install openjpeg-devel -sudo yum install rubygem-rouge rubygem-asciidoctor zbar-devel opencv-devel +sudo yum install rubygem-rouge rubygem-asciidoctor zbar-devel opencv-devel gcc-c++ pkgconfig poppler-cpp-devel python-devel redhat-rpm-config cd /var/www/MISP git clone https://github.com/MISP/misp-modules.git cd misp-modules From 15c257e50434e2926e48281da9f888f854c5eac9 Mon Sep 17 00:00:00 2001 From: Kortho Date: Tue, 18 Jun 2019 10:37:40 +0200 Subject: [PATCH 166/207] changed service pointer Changed so the service starts the modules in the venv where they are installed --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aecdce3..253be61 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ After=misp-workers.service Type=simple User=apache Group=apache -ExecStart=/usr/bin/scl enable rh-python36 rh-ruby22 '/opt/rh/rh-python36/root/bin/misp-modules –l 127.0.0.1 –s' +ExecStart=/usr/bin/scl enable rh-python36 rh-ruby22 '/var/www/MISP/venv/bin/misp-modules –l 127.0.0.1 –s' Restart=always RestartSec=10 From 9a6d484188734867d6504925c33fa912f447bc05 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Fri, 21 Jun 2019 10:53:12 +0200 Subject: [PATCH 167/207] add: Added screenshot of the behavior of the analyzed sample --- misp_modules/lib/joe_parser.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/misp_modules/lib/joe_parser.py b/misp_modules/lib/joe_parser.py index b197ee8..c307399 100644 --- a/misp_modules/lib/joe_parser.py +++ b/misp_modules/lib/joe_parser.py @@ -61,6 +61,7 @@ class JoeParser(): self.parse_system_behavior() self.parse_network_behavior() + self.parse_screenshot() self.parse_network_interactions() self.parse_dropped_files() @@ -140,6 +141,12 @@ class JoeParser(): self.misp_event.add_object(**network_connection_object) self.references[self.analysisinfo_uuid].append({'idref': network_connection_object.uuid, 'relationship': 'initiates'}) + def parse_screenshot(self): + screenshotdata = self.data['behavior']['screenshotdata']['interesting']['$'] + attribute = {'type': 'attachment', 'value': 'screenshot.jpg', + 'data': screenshotdata, 'disable_correlation': True} + self.misp_event.add_attribute(**attribute) + def parse_system_behavior(self): system = self.data['behavior']['system'] if system.get('processes'): From cd062219251e274f5d2f6e51bd1a44d7a5977d14 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 24 Jun 2019 10:22:30 +0200 Subject: [PATCH 168/207] add: [documentation] Added documentation for Joe Sandbox & URLhaus --- doc/README.md | 84 +++++++++++++++++++++++++++ doc/expansion/joesandbox_query.json | 9 +++ doc/expansion/joesandbox_submit.json | 9 +++ doc/expansion/urlhaus.json | 9 +++ doc/import_mod/joeimport.json | 9 +++ doc/logos/joesandbox.png | Bin 0 -> 9780 bytes doc/logos/urlhaus.png | Bin 0 -> 62446 bytes 7 files changed, 120 insertions(+) create mode 100644 doc/expansion/joesandbox_query.json create mode 100644 doc/expansion/joesandbox_submit.json create mode 100644 doc/expansion/urlhaus.json create mode 100644 doc/import_mod/joeimport.json create mode 100644 doc/logos/joesandbox.png create mode 100644 doc/logos/urlhaus.png diff --git a/doc/README.md b/doc/README.md index 02506ab..0ca9b3e 100644 --- a/doc/README.md +++ b/doc/README.md @@ -414,6 +414,52 @@ Module to query IPRep data for IP addresses. ----- +#### [joesandbox_query](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/joesandbox_query.py) + + + +Query Joe Sandbox API with a submission url to get the json report and extract its data that is parsed and converted into MISP attributes and objects. + +This url can by the way come from the result of the [joesandbox_submit expansion module](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/joesandbox_submit.py). +- **features**: +>Module using the new format of modules able to return attributes and objects. +> +>The module returns the same results as the import module [joe_import](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/joe_import.py) taking directly the json report as input. +> +>Even if the introspection will allow all kinds of links to call this module, obviously only the ones presenting a sample or url submission in the Joe Sandbox API will return results. +> +>To make it work you will need to fill the 'apikey' configuration with your Joe Sandbox API key and provide a valid link as input. +- **input**: +>Link of a Joe Sandbox sample or url submission. +- **output**: +>MISP attributes & objects parsed from the analysis report. +- **references**: +>https://www.joesecurity.org, https://www.joesandbox.com/ +- **requirements**: +>jbxapi: Joe Sandbox API python3 library + +----- + +#### [joesandbox_submit](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/joesandbox_submit.py) + + + +A module to submit files or URLs to Joe Sandbox for an advanced analysis, and return the link of the submission. +- **features**: +>The module requires a Joe Sandbox API key to submit files or URL, and returns the link of the submitted analysis. +> +>It is then possible, when the analysis is completed, to query the Joe Sandbox API to get the data related to the analysis, using the [joesandbox_query module](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/joesandbox_query.py) directly on this submission link. +- **input**: +>Sample, url (or domain) to submit to Joe Sandbox for an advanced analysis. +- **output**: +>Link of the data in input submitted to Joe Sandbox. +- **references**: +>https://www.joesecurity.org, https://www.joesandbox.com/ +- **requirements**: +>jbxapi: Joe Sandbox API python3 library + +----- + #### [macaddress_io](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/macaddress_io.py) @@ -798,6 +844,24 @@ Module to get information from ThreatMiner. ----- +#### [urlhaus](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/urlhaus.py) + + + +Query of the URLhaus API to get additional information about the input attribute. +- **features**: +>Module using the new format of modules able to return attributes and objects. +> +>The module takes one of the attribute type specified as input, and query the URLhaus API with it. If any result is returned by the API, attributes and objects are created accordingly. +- **input**: +>A domain, hostname, url, ip, md5 or sha256 attribute. +- **output**: +>MISP attributes & objects fetched from the result of the URLhaus API query. +- **references**: +>https://urlhaus.abuse.ch/ + +----- + #### [urlscan](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/urlscan.py) @@ -1231,6 +1295,26 @@ Module to import MISP objects about financial transactions from GoAML files. ----- +#### [joeimport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/joeimport.py) + + + +A module to import data from a Joe Sandbox analysis json report. +- **features**: +>Module using the new format of modules able to return attributes and objects. +> +>The module returns the same results as the expansion module [joesandbox_query](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/joesandbox_query.py) using the submission link of the analysis to get the json report. +> +> +- **input**: +>Json report of a Joe Sandbox analysis. +- **output**: +>MISP attributes & objects parsed from the analysis report. +- **references**: +>https://www.joesecurity.org, https://www.joesandbox.com/ + +----- + #### [mispjson](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/mispjson.py) Module to import MISP JSON format for merging MISP events. diff --git a/doc/expansion/joesandbox_query.json b/doc/expansion/joesandbox_query.json new file mode 100644 index 0000000..1a94edb --- /dev/null +++ b/doc/expansion/joesandbox_query.json @@ -0,0 +1,9 @@ +{ + "description": "Query Joe Sandbox API with a submission url to get the json report and extract its data that is parsed and converted into MISP attributes and objects.\n\nThis url can by the way come from the result of the [joesandbox_submit expansion module](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/joesandbox_submit.py).", + "logo": "logos/joesandbox.png", + "requirements": ["jbxapi: Joe Sandbox API python3 library"], + "input": "Link of a Joe Sandbox sample or url submission.", + "output": "MISP attributes & objects parsed from the analysis report.", + "references": ["https://www.joesecurity.org", "https://www.joesandbox.com/"], + "features": "Module using the new format of modules able to return attributes and objects.\n\nThe module returns the same results as the import module [joe_import](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/joe_import.py) taking directly the json report as input.\n\nEven if the introspection will allow all kinds of links to call this module, obviously only the ones presenting a sample or url submission in the Joe Sandbox API will return results.\n\nTo make it work you will need to fill the 'apikey' configuration with your Joe Sandbox API key and provide a valid link as input." +} diff --git a/doc/expansion/joesandbox_submit.json b/doc/expansion/joesandbox_submit.json new file mode 100644 index 0000000..ce0cb1f --- /dev/null +++ b/doc/expansion/joesandbox_submit.json @@ -0,0 +1,9 @@ +{ + "description": "A module to submit files or URLs to Joe Sandbox for an advanced analysis, and return the link of the submission.", + "logo": "logos/joesandbox.png", + "requirements": ["jbxapi: Joe Sandbox API python3 library"], + "input": "Sample, url (or domain) to submit to Joe Sandbox for an advanced analysis.", + "output": "Link of the data in input submitted to Joe Sandbox.", + "references": ["https://www.joesecurity.org", "https://www.joesandbox.com/"], + "features": "The module requires a Joe Sandbox API key to submit files or URL, and returns the link of the submitted analysis.\n\nIt is then possible, when the analysis is completed, to query the Joe Sandbox API to get the data related to the analysis, using the [joesandbox_query module](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/joesandbox_query.py) directly on this submission link." +} diff --git a/doc/expansion/urlhaus.json b/doc/expansion/urlhaus.json new file mode 100644 index 0000000..8e5cef3 --- /dev/null +++ b/doc/expansion/urlhaus.json @@ -0,0 +1,9 @@ +{ + "description": "Query of the URLhaus API to get additional information about the input attribute.", + "logo": "logos/urlhaus.png", + "requirements": [], + "input": "A domain, hostname, url, ip, md5 or sha256 attribute.", + "output": "MISP attributes & objects fetched from the result of the URLhaus API query.", + "references": ["https://urlhaus.abuse.ch/"], + "features": "Module using the new format of modules able to return attributes and objects.\n\nThe module takes one of the attribute type specified as input, and query the URLhaus API with it. If any result is returned by the API, attributes and objects are created accordingly." +} diff --git a/doc/import_mod/joeimport.json b/doc/import_mod/joeimport.json new file mode 100644 index 0000000..ceba4ab --- /dev/null +++ b/doc/import_mod/joeimport.json @@ -0,0 +1,9 @@ +{ + "description": "A module to import data from a Joe Sandbox analysis json report.", + "logo": "logos/joesandbox.png", + "requirements": [], + "input": "Json report of a Joe Sandbox analysis.", + "output": "MISP attributes & objects parsed from the analysis report.", + "references": ["https://www.joesecurity.org", "https://www.joesandbox.com/"], + "features": "Module using the new format of modules able to return attributes and objects.\n\nThe module returns the same results as the expansion module [joesandbox_query](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/joesandbox_query.py) using the submission link of the analysis to get the json report.\n\n" +} diff --git a/doc/logos/joesandbox.png b/doc/logos/joesandbox.png new file mode 100644 index 0000000000000000000000000000000000000000..8072f6ea18c6817d029e3615546b5b2cc1e96e0f GIT binary patch literal 9780 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 literal 0 HcmV?d00001 diff --git a/doc/logos/urlhaus.png b/doc/logos/urlhaus.png new file mode 100644 index 0000000000000000000000000000000000000000..b2913502e9025f71becde3e24cbd927890cbea18 GIT binary patch literal 62446 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 S!8hpLh0!uV7yN;}`Tqdf2gQj1 literal 0 HcmV?d00001 From 63e5a0342d0fbe20e69d55d93c4f33d43f8b2b53 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 24 Jun 2019 10:50:31 +0200 Subject: [PATCH 169/207] chg: [documentation] Making URLhaus visible from the github page - Because of the white color, the logo was not visible at all --- doc/logos/urlhaus.png | Bin 62446 -> 48474 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/logos/urlhaus.png b/doc/logos/urlhaus.png index b2913502e9025f71becde3e24cbd927890cbea18..3460d81888b6774a9f1b6f4cc903cff6eaf8ee1f 100644 GIT binary patch literal 48474 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 S!8hpLh0!uV7yN;}`Tqdf2gQj1 From d998368dea033c828eccd0836c6b0820cc8ee505 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 24 Jun 2019 16:24:37 +0200 Subject: [PATCH 170/207] add: [documentation] Added some missing documentation for the most recently added modules --- doc/README.md | 188 ++++++++++++++++++ doc/expansion/docx-enrich.json | 9 + doc/expansion/greynoise.json | 9 + doc/expansion/hibp.json | 9 + doc/expansion/macvendors.json | 9 + doc/expansion/ocr-enrich.json | 8 + doc/expansion/ods-enrich.json | 10 + doc/expansion/odt-enrich.json | 9 + doc/expansion/pdf-enrich.json | 9 + doc/expansion/pptx-enrich.json | 9 + doc/expansion/qrcode.json | 9 + doc/expansion/xlsx-enrich.json | 9 + ...sco_firesight_manager_ACL_rule_export.json | 9 + doc/logos/cisco.png | Bin 0 -> 35608 bytes doc/logos/docx.png | Bin 0 -> 8617 bytes doc/logos/greynoise.png | Bin 0 -> 114641 bytes doc/logos/hibp.png | Bin 0 -> 20147 bytes doc/logos/macvendors.png | Bin 0 -> 5064 bytes doc/logos/ods.png | Bin 0 -> 10161 bytes doc/logos/odt.png | Bin 0 -> 13074 bytes doc/logos/pdf.jpg | Bin 0 -> 8005 bytes doc/logos/pptx.png | Bin 0 -> 12347 bytes doc/logos/xlsx.png | Bin 0 -> 10015 bytes 23 files changed, 296 insertions(+) create mode 100644 doc/expansion/docx-enrich.json create mode 100644 doc/expansion/greynoise.json create mode 100644 doc/expansion/hibp.json create mode 100644 doc/expansion/macvendors.json create mode 100644 doc/expansion/ocr-enrich.json create mode 100644 doc/expansion/ods-enrich.json create mode 100644 doc/expansion/odt-enrich.json create mode 100644 doc/expansion/pdf-enrich.json create mode 100644 doc/expansion/pptx-enrich.json create mode 100644 doc/expansion/qrcode.json create mode 100644 doc/expansion/xlsx-enrich.json create mode 100644 doc/export_mod/cisco_firesight_manager_ACL_rule_export.json create mode 100644 doc/logos/cisco.png create mode 100644 doc/logos/docx.png create mode 100644 doc/logos/greynoise.png create mode 100644 doc/logos/hibp.png create mode 100644 doc/logos/macvendors.png create mode 100644 doc/logos/ods.png create mode 100644 doc/logos/odt.png create mode 100644 doc/logos/pdf.jpg create mode 100644 doc/logos/pptx.png create mode 100644 doc/logos/xlsx.png diff --git a/doc/README.md b/doc/README.md index 0ca9b3e..5656105 100644 --- a/doc/README.md +++ b/doc/README.md @@ -253,6 +253,22 @@ A simple DNS expansion service to resolve IP address from domain MISP attributes ----- +#### [docx-enrich](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/docx-enrich.py) + + + +Module to extract freetext from a .docx document. +- **features**: +>The module reads the text contained in a .docx document. The result is passed to the freetext import parser so IoCs can be extracted out of it. +- **input**: +>Attachment attribute containing a .docx document. +- **output**: +>Text and freetext parsed from the document. +- **requirements**: +>docx python library + +----- + #### [domaintools](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/domaintools.py) @@ -348,6 +364,22 @@ Module to query a local copy of Maxmind's Geolite database. ----- +#### [greynoise](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/greynoise.py) + + + +Module to access GreyNoise.io API +- **features**: +>The module takes an IP address as input and queries Greynoise for some additional information about it. The result is returned as text. +- **input**: +>An IP address. +- **output**: +>Additional information about the IP fetched from Greynoise API. +- **references**: +>https://greynoise.io/, https://github.com/GreyNoise-Intelligence/api.greynoise.io + +----- + #### [hashdd](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/hashdd.py) A hover module to check hashes against hashdd.com including NSLR dataset. @@ -362,6 +394,22 @@ A hover module to check hashes against hashdd.com including NSLR dataset. ----- +#### [hibp](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/hibp.py) + + + +Module to access haveibeenpwned.com API. +- **features**: +>The module takes an email address as input and queries haveibeenpwned.com API to find additional information about it. This additional information actually tells if any account using the email address has already been compromised in a data breach. +- **input**: +>An email address +- **output**: +>Additional information about the email address. +- **references**: +>https://haveibeenpwned.com/ + +----- + #### [intelmq_eventdb](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/intelmq_eventdb.py) @@ -483,6 +531,68 @@ MISP hover module for macaddress.io ----- +#### [macvendors](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/macvendors.py) + + + +Module to access Macvendors API. +- **features**: +>The module takes a MAC address as input and queries macvendors.com for some information about it. The API returns the name of the vendor related to the address. +- **input**: +>A MAC address. +- **output**: +>Additional information about the MAC address. +- **references**: +>https://macvendors.com/, https://macvendors.com/api + +----- + +#### [ocr-enrich](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/ocr-enrich.py) + +Module to process some optical character recognition on pictures. +- **features**: +>The module takes an attachment attributes as input and process some optical character recognition on it. The text found is then passed to the Freetext importer to extract potential IoCs. +- **input**: +>A picture attachment. +- **output**: +>Text and freetext fetched from the input picture. +- **requirements**: +>cv2: The OpenCV python library. + +----- + +#### [ods-enrich](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/ods-enrich.py) + + + +Module to extract freetext from a .ods document. +- **features**: +>The module reads the text contained in a .ods document. The result is passed to the freetext import parser so IoCs can be extracted out of it. +- **input**: +>Attachment attribute containing a .ods document. +- **output**: +>Text and freetext parsed from the document. +- **requirements**: +>ezodf: Python package to create/manipulate OpenDocumentFormat files., pandas_ods_reader: Python library to read in ODS files. + +----- + +#### [odt-enrich](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/odt-enrich.py) + + + +Module to extract freetext from a .odt document. +- **features**: +>The module reads the text contained in a .odt document. The result is passed to the freetext import parser so IoCs can be extracted out of it. +- **input**: +>Attachment attribute containing a .odt document. +- **output**: +>Text and freetext parsed from the document. +- **requirements**: +>ODT reader python library. + +----- + #### [onyphe](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/onyphe.py) @@ -606,6 +716,52 @@ Module to get information from AlienVault OTX. ----- +#### [pdf-enrich](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/pdf-enrich.py) + + + +Module to extract freetext from a PDF document. +- **features**: +>The module reads the text contained in a PDF document. The result is passed to the freetext import parser so IoCs can be extracted out of it. +- **input**: +>Attachment attribute containing a PDF document. +- **output**: +>Text and freetext parsed from the document. +- **requirements**: +>pdftotext: Python library to extract text from PDF. + +----- + +#### [pptx-enrich](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/pptx-enrich.py) + + + +Module to extract freetext from a .pptx document. +- **features**: +>The module reads the text contained in a .pptx document. The result is passed to the freetext import parser so IoCs can be extracted out of it. +- **input**: +>Attachment attribute containing a .pptx document. +- **output**: +>Text and freetext parsed from the document. +- **requirements**: +>pptx: Python library to read PowerPoint files. + +----- + +#### [qrcode](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/qrcode.py) + +Module to decode QR codes. +- **features**: +>The module reads the QR code and returns the related address, which can be an URL or a bitcoin address. +- **input**: +>A QR code stored as attachment attribute. +- **output**: +>The URL or bitcoin address the QR code is pointing to. +- **requirements**: +>cv2: The OpenCV python library., pyzbar: Python library to read QR codes. + +----- + #### [rbl](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/rbl.py) Module to check an IPv4 address against known RBLs. @@ -1029,6 +1185,22 @@ An expansion module for IBM X-Force Exchange. ----- +#### [xlsx-enrich](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/xlsx-enrich.py) + + + +Module to extract freetext from a .xlsx document. +- **features**: +>The module reads the text contained in a .xlsx document. The result is passed to the freetext import parser so IoCs can be extracted out of it. +- **input**: +>Attachment attribute containing a .xlsx document. +- **output**: +>Text and freetext parsed from the document. +- **requirements**: +>pandas: Python library to perform data analysis, time series and statistics. + +----- + #### [yara_query](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/yara_query.py) @@ -1083,6 +1255,22 @@ Module to export a MISP event in CEF format. ----- +#### [cisco_firesight_manager_ACL_rule_export](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/cisco_firesight_manager_ACL_rule_export.py) + + + +Module to export malicious network activity attributes to Cisco fireSIGHT manager block rules. +- **features**: +>The module goes through the attributes to find all the network activity ones in order to create block rules for the Cisco fireSIGHT manager. +- **input**: +>Network activity attributes (IPs, URLs). +- **output**: +>Cisco fireSIGHT manager block rules. +- **requirements**: +>Firesight manager console credentials + +----- + #### [goamlexport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/goamlexport.py) diff --git a/doc/expansion/docx-enrich.json b/doc/expansion/docx-enrich.json new file mode 100644 index 0000000..361f63a --- /dev/null +++ b/doc/expansion/docx-enrich.json @@ -0,0 +1,9 @@ +{ + "description": "Module to extract freetext from a .docx document.", + "logo": "logos/docx.png", + "requirements": ["docx python library"], + "input": "Attachment attribute containing a .docx document.", + "output": "Freetext parsed from the document.", + "references": [], + "features": "The module reads the text contained in a .docx document. The result is passed to the freetext import parser so IoCs can be extracted out of it." +} diff --git a/doc/expansion/greynoise.json b/doc/expansion/greynoise.json new file mode 100644 index 0000000..effb027 --- /dev/null +++ b/doc/expansion/greynoise.json @@ -0,0 +1,9 @@ +{ + "description": "Module to access GreyNoise.io API", + "logo": "greynoise.png", + "requirements": [], + "input": "An IP address.", + "output": "Additional information about the IP fetched from Greynoise API.", + "references": ["https://greynoise.io/", "https://github.com/GreyNoise-Intelligence/api.greynoise.io"], + "features": "The module takes an IP address as input and queries Greynoise for some additional information about it. The result is returned as text." +} diff --git a/doc/expansion/hibp.json b/doc/expansion/hibp.json new file mode 100644 index 0000000..3c3ee54 --- /dev/null +++ b/doc/expansion/hibp.json @@ -0,0 +1,9 @@ +{ + "description": "Module to access haveibeenpwned.com API.", + "logo": "logos/hibp.png", + "requirements": [], + "input": "An email address", + "output": "Additional information about the email address.", + "references": ["https://haveibeenpwned.com/"], + "features": "The module takes an email address as input and queries haveibeenpwned.com API to find additional information about it. This additional information actually tells if any account using the email address has already been compromised in a data breach." +} diff --git a/doc/expansion/macvendors.json b/doc/expansion/macvendors.json new file mode 100644 index 0000000..cc10475 --- /dev/null +++ b/doc/expansion/macvendors.json @@ -0,0 +1,9 @@ +{ + "description": "Module to access Macvendors API.", + "logo": "logos/macvendors.png", + "requirements": [], + "input": "A MAC address.", + "output": "Additional information about the MAC address.", + "references": ["https://macvendors.com/", "https://macvendors.com/api"], + "features": "The module takes a MAC address as input and queries macvendors.com for some information about it. The API returns the name of the vendor related to the address." +} diff --git a/doc/expansion/ocr-enrich.json b/doc/expansion/ocr-enrich.json new file mode 100644 index 0000000..fb222f4 --- /dev/null +++ b/doc/expansion/ocr-enrich.json @@ -0,0 +1,8 @@ +{ + "description": "Module to process some optical character recognition on pictures.", + "requirements": ["The OpenCV python library."], + "input": "A picture attachment.", + "output": "Text and freetext fetched from the input picture.", + "references": [], + "features": "The module takes an attachment attributes as input and process some optical character recognition on it. The text found is then passed to the Freetext importer to extract potential IoCs." +} diff --git a/doc/expansion/ods-enrich.json b/doc/expansion/ods-enrich.json new file mode 100644 index 0000000..dda4281 --- /dev/null +++ b/doc/expansion/ods-enrich.json @@ -0,0 +1,10 @@ +{ + "description": "Module to extract freetext from a .ods document.", + "logo": "logos/ods.png", + "requirements": ["ezodf: Python package to create/manipulate OpenDocumentFormat files.", + "pandas_ods_reader: Python library to read in ODS files."], + "input": "Attachment attribute containing a .ods document.", + "output": "Text and freetext parsed from the document.", + "references": [], + "features": "The module reads the text contained in a .ods document. The result is passed to the freetext import parser so IoCs can be extracted out of it." +} diff --git a/doc/expansion/odt-enrich.json b/doc/expansion/odt-enrich.json new file mode 100644 index 0000000..e201c77 --- /dev/null +++ b/doc/expansion/odt-enrich.json @@ -0,0 +1,9 @@ +{ + "description": "Module to extract freetext from a .odt document.", + "logo": "logos/odt.png", + "requirements": ["ODT reader python library."], + "input": "Attachment attribute containing a .odt document.", + "output": "Text and freetext parsed from the document.", + "references": [], + "features": "The module reads the text contained in a .odt document. The result is passed to the freetext import parser so IoCs can be extracted out of it." +} diff --git a/doc/expansion/pdf-enrich.json b/doc/expansion/pdf-enrich.json new file mode 100644 index 0000000..5b3f0a8 --- /dev/null +++ b/doc/expansion/pdf-enrich.json @@ -0,0 +1,9 @@ +{ + "description": "Module to extract freetext from a PDF document.", + "logo": "logos/pdf.jpg", + "requirements": ["pdftotext: Python library to extract text from PDF."], + "input": "Attachment attribute containing a PDF document.", + "output": "Text and freetext parsed from the document.", + "references": [], + "features": "The module reads the text contained in a PDF document. The result is passed to the freetext import parser so IoCs can be extracted out of it." +} diff --git a/doc/expansion/pptx-enrich.json b/doc/expansion/pptx-enrich.json new file mode 100644 index 0000000..aff0d8d --- /dev/null +++ b/doc/expansion/pptx-enrich.json @@ -0,0 +1,9 @@ +{ + "description": "Module to extract freetext from a .pptx document.", + "logo": "logos/pptx.png", + "requirements": ["pptx: Python library to read PowerPoint files."], + "input": "Attachment attribute containing a .pptx document.", + "output": "Text and freetext parsed from the document.", + "references": [], + "features": "The module reads the text contained in a .pptx document. The result is passed to the freetext import parser so IoCs can be extracted out of it." +} diff --git a/doc/expansion/qrcode.json b/doc/expansion/qrcode.json new file mode 100644 index 0000000..38ed77c --- /dev/null +++ b/doc/expansion/qrcode.json @@ -0,0 +1,9 @@ +{ + "description": "Module to decode QR codes.", + "requirements": ["cv2: The OpenCV python library.", + "pyzbar: Python library to read QR codes."], + "input": "A QR code stored as attachment attribute.", + "output": "The URL or bitcoin address the QR code is pointing to.", + "references": [], + "features": "The module reads the QR code and returns the related address, which can be an URL or a bitcoin address." +} diff --git a/doc/expansion/xlsx-enrich.json b/doc/expansion/xlsx-enrich.json new file mode 100644 index 0000000..c41f17c --- /dev/null +++ b/doc/expansion/xlsx-enrich.json @@ -0,0 +1,9 @@ +{ + "description": "Module to extract freetext from a .xlsx document.", + "logo": "logos/xlsx.png", + "requirements": ["pandas: Python library to perform data analysis, time series and statistics."], + "input": "Attachment attribute containing a .xlsx document.", + "output": "Text and freetext parsed from the document.", + "references": [], + "features": "The module reads the text contained in a .xlsx document. The result is passed to the freetext import parser so IoCs can be extracted out of it." +} diff --git a/doc/export_mod/cisco_firesight_manager_ACL_rule_export.json b/doc/export_mod/cisco_firesight_manager_ACL_rule_export.json new file mode 100644 index 0000000..6d1d0dd --- /dev/null +++ b/doc/export_mod/cisco_firesight_manager_ACL_rule_export.json @@ -0,0 +1,9 @@ +{ + "description": "Module to export malicious network activity attributes to Cisco fireSIGHT manager block rules.", + "logo": "logos/cisco.png", + "requirements": ["Firesight manager console credentials"], + "input": "Network activity attributes (IPs, URLs).", + "output": "Cisco fireSIGHT manager block rules.", + "references": [], + "features": "The module goes through the attributes to find all the network activity ones in order to create block rules for the Cisco fireSIGHT manager." +} diff --git a/doc/logos/cisco.png b/doc/logos/cisco.png new file mode 100644 index 0000000000000000000000000000000000000000..87b863b7736c9d029a4b9f29ed5cce0e055945c9 GIT binary patch literal 35608 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 literal 0 HcmV?d00001 diff --git a/doc/logos/docx.png b/doc/logos/docx.png new file mode 100644 index 0000000000000000000000000000000000000000..018d2c1af88c652429c8a00d11e974a4af386eff GIT binary patch literal 8617 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 literal 0 HcmV?d00001 diff --git a/doc/logos/greynoise.png b/doc/logos/greynoise.png new file mode 100644 index 0000000000000000000000000000000000000000..b4d4f9167a556040a6191b4dfd6d82e27f6e2713 GIT binary patch literal 114641 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 O1B0ilpUXO@geCxUa!>{U literal 0 HcmV?d00001 diff --git a/doc/logos/hibp.png b/doc/logos/hibp.png new file mode 100644 index 0000000000000000000000000000000000000000..849ccf27e525cdfc4c01196a6d55dde96e2518a8 GIT binary patch literal 20147 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 SyTLzn5EH|j2DN%FQU3?S5@dk@ literal 0 HcmV?d00001 diff --git a/doc/logos/ods.png b/doc/logos/ods.png new file mode 100644 index 0000000000000000000000000000000000000000..19b42f1f6b0e31faa1beeb8c0a7f07c95cdc80f4 GIT binary patch literal 10161 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 literal 0 HcmV?d00001 diff --git a/doc/logos/odt.png b/doc/logos/odt.png new file mode 100644 index 0000000000000000000000000000000000000000..d177a217d2155cd9081528989f556e9f4214489f GIT binary patch literal 13074 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 XB2l+KA-8H^uo3y_?O$sbbUX83XNx!S literal 0 HcmV?d00001 diff --git a/doc/logos/pptx.png b/doc/logos/pptx.png new file mode 100644 index 0000000000000000000000000000000000000000..11b2133765e9048106c1cbb0c4d67b60d73b8566 GIT binary patch literal 12347 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 literal 0 HcmV?d00001 From aa56248110398362ff474bf7544532e44a53c809 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 24 Jun 2019 16:26:36 +0200 Subject: [PATCH 171/207] fix: [documentation] Fixed some description & logo --- doc/expansion/docx-enrich.json | 2 +- doc/expansion/greynoise.json | 2 +- doc/expansion/ocr-enrich.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/expansion/docx-enrich.json b/doc/expansion/docx-enrich.json index 361f63a..fccba57 100644 --- a/doc/expansion/docx-enrich.json +++ b/doc/expansion/docx-enrich.json @@ -3,7 +3,7 @@ "logo": "logos/docx.png", "requirements": ["docx python library"], "input": "Attachment attribute containing a .docx document.", - "output": "Freetext parsed from the document.", + "output": "Text and freetext parsed from the document.", "references": [], "features": "The module reads the text contained in a .docx document. The result is passed to the freetext import parser so IoCs can be extracted out of it." } diff --git a/doc/expansion/greynoise.json b/doc/expansion/greynoise.json index effb027..f1f1003 100644 --- a/doc/expansion/greynoise.json +++ b/doc/expansion/greynoise.json @@ -1,6 +1,6 @@ { "description": "Module to access GreyNoise.io API", - "logo": "greynoise.png", + "logo": "logos/greynoise.png", "requirements": [], "input": "An IP address.", "output": "Additional information about the IP fetched from Greynoise API.", diff --git a/doc/expansion/ocr-enrich.json b/doc/expansion/ocr-enrich.json index fb222f4..8765b22 100644 --- a/doc/expansion/ocr-enrich.json +++ b/doc/expansion/ocr-enrich.json @@ -1,6 +1,6 @@ { "description": "Module to process some optical character recognition on pictures.", - "requirements": ["The OpenCV python library."], + "requirements": ["cv2: The OpenCV python library."], "input": "A picture attachment.", "output": "Text and freetext fetched from the input picture.", "references": [], From 6b59963a7fb4b7f8d38d7e2e53049cb13d4643c4 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 24 Jun 2019 16:34:22 +0200 Subject: [PATCH 172/207] fix: [documentation] Fixed json file name --- doc/README.md | 2 +- doc/import_mod/{joeimport.json => joe_import.json} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename doc/import_mod/{joeimport.json => joe_import.json} (100%) diff --git a/doc/README.md b/doc/README.md index 5656105..fd5b73b 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1483,7 +1483,7 @@ Module to import MISP objects about financial transactions from GoAML files. ----- -#### [joeimport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/joeimport.py) +#### [joe_import](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/joe_import.py) diff --git a/doc/import_mod/joeimport.json b/doc/import_mod/joe_import.json similarity index 100% rename from doc/import_mod/joeimport.json rename to doc/import_mod/joe_import.json From 181e6383a363e49b804d473f9fbff0c565c54e55 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Wed, 3 Jul 2019 11:14:46 +0200 Subject: [PATCH 173/207] fix: Added missing add_attribute function --- misp_modules/lib/joe_parser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/misp_modules/lib/joe_parser.py b/misp_modules/lib/joe_parser.py index c307399..d957d69 100644 --- a/misp_modules/lib/joe_parser.py +++ b/misp_modules/lib/joe_parser.py @@ -259,6 +259,7 @@ class JoeParser(): else: attribute = MISPAttribute() attribute.from_dict(**{'type': 'domain', 'value': domain['@name']}) + self.misp_event.add_attribute(**attribute) reference = {'idref': attribute.uuid, 'relationship': 'contacts'} self.add_process_reference(domain['@targetid'], domain['@currentpath'], reference) ipinfo = self.data['ipinfo'] From 5703253961ee8df7e12c51f1a134c5c291f423b9 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Wed, 10 Jul 2019 15:20:22 +0200 Subject: [PATCH 174/207] new: First version of an advanced CVE parser module - Using cve.circl.lu as well as the initial module - Going deeper into the CVE parsing - More parsing to come with the CWE, CAPEC and so on --- misp_modules/modules/expansion/__init__.py | 2 +- .../modules/expansion/cve_advanced.py | 73 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 misp_modules/modules/expansion/cve_advanced.py diff --git a/misp_modules/modules/expansion/__init__.py b/misp_modules/modules/expansion/__init__.py index acf49f2..960db23 100644 --- a/misp_modules/modules/expansion/__init__.py +++ b/misp_modules/modules/expansion/__init__.py @@ -4,7 +4,7 @@ import sys sys.path.append('{}/lib'.format('/'.join((os.path.realpath(__file__)).split('/')[:-3]))) __all__ = ['cuckoo_submit', 'vmray_submit', 'bgpranking', 'circl_passivedns', 'circl_passivessl', - 'countrycode', 'cve', 'dns', 'btc_steroids', 'domaintools', 'eupi', + 'countrycode', 'cve', 'cve_advanced', 'dns', 'btc_steroids', 'domaintools', 'eupi', 'farsight_passivedns', 'ipasn', 'passivetotal', 'sourcecache', 'virustotal', 'whois', 'shodan', 'reversedns', 'geoip_country', 'wiki', 'iprep', 'threatminer', 'otx', 'threatcrowd', 'vulndb', 'crowdstrike_falcon', diff --git a/misp_modules/modules/expansion/cve_advanced.py b/misp_modules/modules/expansion/cve_advanced.py new file mode 100644 index 0000000..f245875 --- /dev/null +++ b/misp_modules/modules/expansion/cve_advanced.py @@ -0,0 +1,73 @@ +from pymisp import MISPAttribute, MISPEvent, MISPObject +import json +import requests + +misperrors = {'error': 'Error'} +mispattributes = {'input': ['vulnerability'], 'format': 'misp_standard'} +moduleinfo = {'version': '1', 'author': 'Christian Studer', 'description': 'An expansion module to enrich a CVE attribute with the vulnerability information.', 'module-type': ['expansion', 'hover']} +moduleconfig = [] +cveapi_url = 'https://cve.circl.lu/api/cve/' + + +class VulnerabilityParser(): + def __init__(self, vulnerability): + self.vulnerability = vulnerability + self.misp_event = MISPEvent() + self.vulnerability_mapping = { + 'id': ('text', 'id'), 'summary': ('text', 'summary'), + 'vulnerable_configuration_cpe_2_2': ('text', 'vulnerable_configuration'), + 'Modified': ('datetime', 'modified'), 'Published': ('datetime', 'published'), + 'references': ('link', 'references'), 'cvss': ('float', 'cvss')} + + def get_result(self): + event = json.loads(self.misp_event.to_json())['Event'] + results = {key: event[key] for key in ('Attribute', 'Object') if (key in event and event[key])} + return {'results': results} + + def parse_vulnerability_information(self): + vulnerability_object = MISPObject('vulnerability') + for feature in ('id', 'summary', 'Modified', 'cvss'): + value = self.vulnerability.get(feature) + if value: + attribute_type, relation = self.vulnerability_mapping[feature] + vulnerability_object.add_attribute(relation, **{'type': attribute_type, 'value': value}) + if 'Published' in self.vulnerability: + vulnerability_object.add_attribute('published', **{'type': 'datetime', 'value': self.vulnerability['Published']}) + vulnerability_object.add_attribute('state', **{'type': 'text', 'value': 'Published'}) + for feature in ('references', 'vulnerable_configuration_cpe_2_2'): + if feature in self.vulnerability: + attribute_type, relation = self.vulnerability_mapping[feature] + for value in self.vulnerability[feature]: + vulnerability_object.add_attribute(relation, **{'type': attribute_type, 'value': value}) + self.misp_event.add_object(**vulnerability_object) + + +def handler(q=False): + if q is False: + return False + request = json.loads(q) + attribute = request.get('attribute') + if attribute.get('type') != 'vulnerability': + misperrors['error'] = 'Vulnerability id missing.' + return misperrors + r = requests.get("{}{}".format(cveapi_url, attribute['value'])) + if r.status_code == 200: + vulnerability = r.json() + if not vulnerability: + misperrors['error'] = 'Non existing CVE' + return misperrors['error'] + else: + misperrors['error'] = 'cve.circl.lu API not accessible' + return misperrors['error'] + parser = VulnerabilityParser(vulnerability) + parser.parse_vulnerability_information() + return parser.get_result() + + +def introspection(): + return mispattributes + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo From 3edc323836fcb65f91db644bf80d80719d66e774 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Wed, 10 Jul 2019 15:29:31 +0200 Subject: [PATCH 175/207] fix: Making pep8 happy --- misp_modules/modules/expansion/cve_advanced.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/cve_advanced.py b/misp_modules/modules/expansion/cve_advanced.py index f245875..3a89ec9 100644 --- a/misp_modules/modules/expansion/cve_advanced.py +++ b/misp_modules/modules/expansion/cve_advanced.py @@ -1,4 +1,4 @@ -from pymisp import MISPAttribute, MISPEvent, MISPObject +from pymisp import MISPEvent, MISPObject import json import requests From ade4b98588630e706eb09699fc48681b8ba52b15 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Wed, 10 Jul 2019 15:30:19 +0200 Subject: [PATCH 176/207] add: Updated README file with the new module description --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1740fc0..c9fd915 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ * [countrycode](misp_modules/modules/expansion/countrycode.py) - a hover module to tell you what country a URL belongs to. * [CrowdStrike Falcon](misp_modules/modules/expansion/crowdstrike_falcon.py) - an expansion module to expand using CrowdStrike Falcon Intel Indicator API. * [CVE](misp_modules/modules/expansion/cve.py) - a hover module to give more information about a vulnerability (CVE). +* [CVE advanced](misp_modules/modules/expansion/cve_advanced.py) - An expansion module to query the CIRCL CVE search API for more information about a vulnerability (CVE). * [Cuckoo submit](misp_modules/modules/expansion/cuckoo_submit.py) - A hover module to submit malware sample, url, attachment, domain to Cuckoo Sandbox. * [DBL Spamhaus](misp_modules/modules/expansion/dbl_spamhaus.py) - a hover module to check Spamhaus DBL for a domain name. * [DNS](misp_modules/modules/expansion/dns.py) - a simple module to resolve MISP attributes like hostname and domain to expand IP addresses attributes. From f862a14ce68f8935bb54fbc1250881770cc38cc7 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Thu, 11 Jul 2019 22:59:07 +0200 Subject: [PATCH 177/207] add: Object for VirusTotal public API queries - Lighter analysis of the report to avoid reaching the limit of queries per minute while recursing on the different elements --- .../modules/expansion/virustotal_public.py | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 misp_modules/modules/expansion/virustotal_public.py diff --git a/misp_modules/modules/expansion/virustotal_public.py b/misp_modules/modules/expansion/virustotal_public.py new file mode 100644 index 0000000..2b84748 --- /dev/null +++ b/misp_modules/modules/expansion/virustotal_public.py @@ -0,0 +1,174 @@ +from pymisp import MISPAttribute, MISPEvent, MISPObject +import json +import requests + +misperrors = {'error': 'Error'} +mispattributes = {'input': ['hostname', 'domain', "ip-src", "ip-dst", "md5", "sha1", "sha256", "sha512", "url"], + 'format': 'misp_standard'} +moduleinfo = {'version': '1', 'author': 'Christian Studer', + 'description': 'Get information from virustotal public API v2.', + 'module_type': ['expansion', 'hover']} + +moduleconfig = ['apikey'] + + +class VirusTotalParser(): + def __init__(self): + super(VirusTotalParser, self).__init__() + self.misp_event = MISPEvent() + + def declare_variables(self, apikey, attribute): + self.attribute = MISPAttribute() + self.attribute.from_dict(**attribute) + self.apikey = apikey + + def get_result(self): + event = json.loads(self.misp_event.to_json())['Event'] + results = {key: event[key] for key in ('Attribute', 'Object') if (key in event and event[key])} + return {'results': results} + + def parse_detected_urls(detected_urls): + for url in detected_urls: + self.misp_event.add_attribute('url', url) + + def parse_resolutions(self, resolutions, subdomains=None): + domain_ip_object = MISPObject('domain-ip') + domain_ip_object.add_attribute('ip', type='ip-dst', value=self.attribute.value) + for resolution in resolutions: + domain_ip_object.add_attribute('domain', type='domain', value=resolution['hostname']) + if subdomains: + for subdomain in subdomains: + attribute = MISPAttribute() + attribute.from_dict(**dict(type='domain', value=subdomain)) + self.misp_event.add_attribute(**attribute) + domain_ip_object.add_reference(attribute.uuid, 'subdomain') + self.misp_event.add_object(**domain_ip_object) + + def parse_vt_object(self, query_result): + vt_object = MISPObject('virustotal-report') + vt_object.add_attribute('permalink', type='link', value=query_result['permalink']) + detection_ratio = '{}/{}'.format(query_result['positives'], query_result['total']) + vt_object.add_object('detection-ratio', type='text', value=detection_ratio) + self.misp_event.add_object(**vt_object) + + def query_result(self, query_type): + params = {query_type: self.attribute.value, 'apikey': self.apikey} + return requests.get(self.base_url, params=params) + + +class DomainQuery(VirusTotalParser): + def __init__(self, apikey, attribute): + super(DomainQuery, self).__init__() + self.base_url = "https://www.virustotal.com/vtapi/v2/domain/report" + self.declare_variables(apikey, attribute) + + def parse_report(self, query_result): + hash_type = 'sha256' + whois = 'whois' + for feature in ('undetected_referrer_samples', 'detected_referrer_samples'): + for sample in query_result[feature]: + self.misp_event.add_attribute(has_type, sample[hash_type]) + if query_result.get(whois): + self.misp_event.add_attribute(whois, query_result[whois]) + self.parse_resolutions(query_result['resolutions'], query_result['subdomains']) + self.parse_detected_urls(query_result['detected_urls']) + for domain in query_result['domain_siblings']: + self.misp_event.add_attribute('domain', domain) + + +class HashQuery(VirusTotalParser): + def __init__(self, apikey, attribute): + super(HashQuery, self).__init__() + self.base_url = "https://www.virustotal.com/vtapi/v2/file/report" + self.declare_variables(apikey, attribute) + + def parse_report(self, query_result): + file_attributes = [] + for hash_type in ('md5', 'sha1', 'sha256'): + if query_request.get(hash_type): + file_attributes.append({'type': hash_type, 'object_relation': hash_type, + 'value': query_request[hash_type]}) + if file_attributes: + file_object = MISPOBject('file') + for attribute in file_attributes: + file_object.add_attribute(**attribute) + self.misp_event.add_object(**file_object) + self.parse_vt_object(query_result) + + +class IpQuery(VirusTotalParser): + def __init__(self, apikey, attribute): + super(IpQuery, self).__init__() + self.base_url = "https://www.virustotal.com/vtapi/v2/ip-address/report" + self.declare_variables(apikey, attribute) + + def parse_report(self, query_result): + if query_result.get('asn'): + asn_mapping = {'network': ('ip-src', 'subnet-announced'), + 'country': {'text', 'country'}} + asn_object = MISPObject('asn') + asn_object.add_attribute('asn', type='AS', value=query_result['asn']) + for key, value in asn_mapping.items(): + if query.get(key): + attribute_type, relation = asn_mapping[key] + asn_object.add_attribute(relation, type=attribute_type, value=value) + self.misp_event.add_object(**asn_object) + self.parse_detected_urls(query_result['detected_urls']) + if query_result.get('resolutions'): + self.parse_resolutions(query_result['resolutions']) + + +class UrlQuery(VirusTotalParser): + def __init__(self, apikey, attribute): + super(UrlQuery, self).__init__() + self.base_url = "https://www.virustotal.com/vtapi/v2/url/report" + self.declare_variables(apikey, attribute) + + def parse_report(self, query_result): + self.parse_vt_object(query_result) + + +domain = ('domain', DomainQuery) +ip = ('ip', IpQuery) +file = ('resource', HashQuery) +misp_type_mapping = {'domain': domain, 'hostname': domain, 'ip-src': ip, + 'ip-dst': ip, 'md5': file, 'sha1': file, 'sha256': file, + 'sha512': file, 'url': ('resource', UrlQuery)} + + +def parse_error(status_code): + status_mapping = {204: 'VirusTotal request rate limit exceeded.', + 400: 'Incorrect request, please check the arguments.', + 403: 'You don\'t have enough privileges to make the request.'} + if status_code in status_mapping: + return status_mapping[status_code] + return "VirusTotal may not be accessible." + + +def handler(q=False): + if q is False: + return False + request = json.loads(q) + if not request.get('config') or not request['config'].get('apikey'): + misperrors['error'] = "A VirusTotal api key is required for this module." + return misperrors + attribute = request['attribute'] + query_type, to_call = misp_type_mapping[attribute['type']] + parser = to_call(request['config']['apikey'], attribute) + query_result = parser.query_result(query_type) + status_code = query_result.status_code + if status_code == 200: + parser.parse_report(query_result.json()) + else: + misperrors['error'] = parse_error(status_code) + return misperrors + return parser.get_result() + + +def introspection(): + return mispattributes + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo From d9b03a7aa5e638172e14e5058b06e9e6202b798c Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Fri, 12 Jul 2019 10:59:19 +0200 Subject: [PATCH 178/207] fix: Various fixes about typo, variable names, data types and so on --- .../modules/expansion/virustotal_public.py | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/misp_modules/modules/expansion/virustotal_public.py b/misp_modules/modules/expansion/virustotal_public.py index 2b84748..a2d5dd3 100644 --- a/misp_modules/modules/expansion/virustotal_public.py +++ b/misp_modules/modules/expansion/virustotal_public.py @@ -7,7 +7,7 @@ mispattributes = {'input': ['hostname', 'domain', "ip-src", "ip-dst", "md5", "sh 'format': 'misp_standard'} moduleinfo = {'version': '1', 'author': 'Christian Studer', 'description': 'Get information from virustotal public API v2.', - 'module_type': ['expansion', 'hover']} + 'module-type': ['expansion', 'hover']} moduleconfig = ['apikey'] @@ -27,15 +27,21 @@ class VirusTotalParser(): results = {key: event[key] for key in ('Attribute', 'Object') if (key in event and event[key])} return {'results': results} - def parse_detected_urls(detected_urls): + def parse_detected_urls(self, detected_urls): for url in detected_urls: - self.misp_event.add_attribute('url', url) + value = url['url'] if isinstance(url, dict) else url + self.misp_event.add_attribute('url', value) def parse_resolutions(self, resolutions, subdomains=None): domain_ip_object = MISPObject('domain-ip') - domain_ip_object.add_attribute('ip', type='ip-dst', value=self.attribute.value) + if self.attribute.type == 'domain': + domain_ip_object.add_attribute('domain', type='domain', value=self.attribute.value) + attribute_type, relation, key = ('ip-dst', 'ip', 'ip_address') + else: + domain_ip_object.add_attribute('ip', type='ip-dst', value=self.attribute.value) + attribute_type, relation, key = ('domain', 'domain', 'hostname') for resolution in resolutions: - domain_ip_object.add_attribute('domain', type='domain', value=resolution['hostname']) + domain_ip_object.add_attribute(relation, type=attribute_type, value=resolution[key]) if subdomains: for subdomain in subdomains: attribute = MISPAttribute() @@ -48,7 +54,7 @@ class VirusTotalParser(): vt_object = MISPObject('virustotal-report') vt_object.add_attribute('permalink', type='link', value=query_result['permalink']) detection_ratio = '{}/{}'.format(query_result['positives'], query_result['total']) - vt_object.add_object('detection-ratio', type='text', value=detection_ratio) + vt_object.add_attribute('detection-ratio', type='text', value=detection_ratio) self.misp_event.add_object(**vt_object) def query_result(self, query_type): @@ -67,9 +73,11 @@ class DomainQuery(VirusTotalParser): whois = 'whois' for feature in ('undetected_referrer_samples', 'detected_referrer_samples'): for sample in query_result[feature]: - self.misp_event.add_attribute(has_type, sample[hash_type]) + self.misp_event.add_attribute(hash_type, sample[hash_type]) if query_result.get(whois): - self.misp_event.add_attribute(whois, query_result[whois]) + whois_object = MISPObject(whois) + whois_object.add_attribute('text', type='text', value=query_result[whois]) + self.misp_event.add_object(**whois_object) self.parse_resolutions(query_result['resolutions'], query_result['subdomains']) self.parse_detected_urls(query_result['detected_urls']) for domain in query_result['domain_siblings']: @@ -85,11 +93,11 @@ class HashQuery(VirusTotalParser): def parse_report(self, query_result): file_attributes = [] for hash_type in ('md5', 'sha1', 'sha256'): - if query_request.get(hash_type): + if query_result.get(hash_type): file_attributes.append({'type': hash_type, 'object_relation': hash_type, - 'value': query_request[hash_type]}) + 'value': query_result[hash_type]}) if file_attributes: - file_object = MISPOBject('file') + file_object = MISPObject('file') for attribute in file_attributes: file_object.add_attribute(**attribute) self.misp_event.add_object(**file_object) @@ -105,13 +113,13 @@ class IpQuery(VirusTotalParser): def parse_report(self, query_result): if query_result.get('asn'): asn_mapping = {'network': ('ip-src', 'subnet-announced'), - 'country': {'text', 'country'}} + 'country': ('text', 'country')} asn_object = MISPObject('asn') asn_object.add_attribute('asn', type='AS', value=query_result['asn']) for key, value in asn_mapping.items(): - if query.get(key): - attribute_type, relation = asn_mapping[key] - asn_object.add_attribute(relation, type=attribute_type, value=value) + if query_result.get(key): + attribute_type, relation = value + asn_object.add_attribute(relation, type=attribute_type, value=query_result[key]) self.misp_event.add_object(**asn_object) self.parse_detected_urls(query_result['detected_urls']) if query_result.get('resolutions'): From a61d09db8b6aa68c69d191f659e267870a25fb47 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 15 Jul 2019 23:44:25 +0200 Subject: [PATCH 179/207] fix: Parsing detected & undetected urls --- misp_modules/modules/expansion/virustotal_public.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/misp_modules/modules/expansion/virustotal_public.py b/misp_modules/modules/expansion/virustotal_public.py index a2d5dd3..0d50a86 100644 --- a/misp_modules/modules/expansion/virustotal_public.py +++ b/misp_modules/modules/expansion/virustotal_public.py @@ -27,10 +27,11 @@ class VirusTotalParser(): results = {key: event[key] for key in ('Attribute', 'Object') if (key in event and event[key])} return {'results': results} - def parse_detected_urls(self, detected_urls): - for url in detected_urls: - value = url['url'] if isinstance(url, dict) else url - self.misp_event.add_attribute('url', value) + def parse_urls(self, query_result): + for feature in ('detected_urls', 'undetected_urls'): + for url in query_result[feature]: + value = url['url'] if isinstance(url, dict) else url + self.misp_event.add_attribute('url', value) def parse_resolutions(self, resolutions, subdomains=None): domain_ip_object = MISPObject('domain-ip') @@ -79,7 +80,7 @@ class DomainQuery(VirusTotalParser): whois_object.add_attribute('text', type='text', value=query_result[whois]) self.misp_event.add_object(**whois_object) self.parse_resolutions(query_result['resolutions'], query_result['subdomains']) - self.parse_detected_urls(query_result['detected_urls']) + self.parse_urls(query_result) for domain in query_result['domain_siblings']: self.misp_event.add_attribute('domain', domain) @@ -121,7 +122,7 @@ class IpQuery(VirusTotalParser): attribute_type, relation = value asn_object.add_attribute(relation, type=attribute_type, value=query_result[key]) self.misp_event.add_object(**asn_object) - self.parse_detected_urls(query_result['detected_urls']) + self.parse_urls(query_result) if query_result.get('resolutions'): self.parse_resolutions(query_result['resolutions']) From 8de350744b86f7ddcddfaedb8be65af6c973e02b Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Tue, 16 Jul 2019 22:39:35 +0200 Subject: [PATCH 180/207] chg: Getting domain siblings attributes uuid for further references --- misp_modules/modules/expansion/virustotal_public.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/misp_modules/modules/expansion/virustotal_public.py b/misp_modules/modules/expansion/virustotal_public.py index 0d50a86..6e5a58d 100644 --- a/misp_modules/modules/expansion/virustotal_public.py +++ b/misp_modules/modules/expansion/virustotal_public.py @@ -79,10 +79,15 @@ class DomainQuery(VirusTotalParser): whois_object = MISPObject(whois) whois_object.add_attribute('text', type='text', value=query_result[whois]) self.misp_event.add_object(**whois_object) + siblings = (self.parse_siblings(domain) for domain in query_result['domain_siblings']) self.parse_resolutions(query_result['resolutions'], query_result['subdomains']) self.parse_urls(query_result) - for domain in query_result['domain_siblings']: - self.misp_event.add_attribute('domain', domain) + + def parse_siblings(domain): + attribute = MISPAttribute() + attribute.from_dict(dict(type='domain', value=domain)) + self.misp_event.add_attribute(**attribute) + return attribute.uuid class HashQuery(VirusTotalParser): From 795edb7457decbc258aed9ea8594b0431a0ffcc7 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Wed, 17 Jul 2019 20:40:56 +0200 Subject: [PATCH 181/207] chg: Adding references between a domain and their siblings --- misp_modules/modules/expansion/virustotal_public.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/misp_modules/modules/expansion/virustotal_public.py b/misp_modules/modules/expansion/virustotal_public.py index 6e5a58d..faababc 100644 --- a/misp_modules/modules/expansion/virustotal_public.py +++ b/misp_modules/modules/expansion/virustotal_public.py @@ -33,7 +33,7 @@ class VirusTotalParser(): value = url['url'] if isinstance(url, dict) else url self.misp_event.add_attribute('url', value) - def parse_resolutions(self, resolutions, subdomains=None): + def parse_resolutions(self, resolutions, subdomains=None, uuids=None): domain_ip_object = MISPObject('domain-ip') if self.attribute.type == 'domain': domain_ip_object.add_attribute('domain', type='domain', value=self.attribute.value) @@ -49,6 +49,9 @@ class VirusTotalParser(): attribute.from_dict(**dict(type='domain', value=subdomain)) self.misp_event.add_attribute(**attribute) domain_ip_object.add_reference(attribute.uuid, 'subdomain') + if uuids: + for uuid in uuids: + domain_ip_object.add_reference(uuid, 'sibling-of') self.misp_event.add_object(**domain_ip_object) def parse_vt_object(self, query_result): @@ -80,7 +83,7 @@ class DomainQuery(VirusTotalParser): whois_object.add_attribute('text', type='text', value=query_result[whois]) self.misp_event.add_object(**whois_object) siblings = (self.parse_siblings(domain) for domain in query_result['domain_siblings']) - self.parse_resolutions(query_result['resolutions'], query_result['subdomains']) + self.parse_resolutions(query_result['resolutions'], query_result['subdomains'], siblings) self.parse_urls(query_result) def parse_siblings(domain): From 641dda010393d638327e5cca9a72e84b84f5f956 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Thu, 18 Jul 2019 21:38:17 +0200 Subject: [PATCH 182/207] add: Parsing downloaded samples as well as the referrer ones --- misp_modules/modules/expansion/virustotal_public.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/misp_modules/modules/expansion/virustotal_public.py b/misp_modules/modules/expansion/virustotal_public.py index faababc..46a636e 100644 --- a/misp_modules/modules/expansion/virustotal_public.py +++ b/misp_modules/modules/expansion/virustotal_public.py @@ -75,9 +75,10 @@ class DomainQuery(VirusTotalParser): def parse_report(self, query_result): hash_type = 'sha256' whois = 'whois' - for feature in ('undetected_referrer_samples', 'detected_referrer_samples'): - for sample in query_result[feature]: - self.misp_event.add_attribute(hash_type, sample[hash_type]) + for feature_type in ('referrer', 'dowloaded'): + for feature in ('undetected_{}_samples', 'detected_{}_samples'): + for sample in query_result[feature.format(feature_type)]: + self.misp_event.add_attribute(hash_type, sample[hash_type]) if query_result.get(whois): whois_object = MISPObject(whois) whois_object.add_attribute('text', type='text', value=query_result[whois]) From 9aa721bc37dd36b320dac80611a8b2730723d038 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Fri, 19 Jul 2019 16:20:24 +0200 Subject: [PATCH 183/207] fix: typo --- misp_modules/modules/expansion/virustotal_public.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/virustotal_public.py b/misp_modules/modules/expansion/virustotal_public.py index 46a636e..619e3cd 100644 --- a/misp_modules/modules/expansion/virustotal_public.py +++ b/misp_modules/modules/expansion/virustotal_public.py @@ -75,7 +75,7 @@ class DomainQuery(VirusTotalParser): def parse_report(self, query_result): hash_type = 'sha256' whois = 'whois' - for feature_type in ('referrer', 'dowloaded'): + for feature_type in ('referrer', 'downloaded'): for feature in ('undetected_{}_samples', 'detected_{}_samples'): for sample in query_result[feature.format(feature_type)]: self.misp_event.add_attribute(hash_type, sample[hash_type]) From 729c86c3363e41964af8521505b2687f270bf33b Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 22 Jul 2019 09:16:04 +0200 Subject: [PATCH 184/207] fix: Quick fix on siblings & url parsing --- misp_modules/modules/expansion/virustotal_public.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/misp_modules/modules/expansion/virustotal_public.py b/misp_modules/modules/expansion/virustotal_public.py index 619e3cd..12c73d5 100644 --- a/misp_modules/modules/expansion/virustotal_public.py +++ b/misp_modules/modules/expansion/virustotal_public.py @@ -29,9 +29,10 @@ class VirusTotalParser(): def parse_urls(self, query_result): for feature in ('detected_urls', 'undetected_urls'): - for url in query_result[feature]: - value = url['url'] if isinstance(url, dict) else url - self.misp_event.add_attribute('url', value) + if feature in query_result: + for url in query_result[feature]: + value = url['url'] if isinstance(url, dict) else url + self.misp_event.add_attribute('url', value) def parse_resolutions(self, resolutions, subdomains=None, uuids=None): domain_ip_object = MISPObject('domain-ip') @@ -87,9 +88,9 @@ class DomainQuery(VirusTotalParser): self.parse_resolutions(query_result['resolutions'], query_result['subdomains'], siblings) self.parse_urls(query_result) - def parse_siblings(domain): + def parse_siblings(self, domain): attribute = MISPAttribute() - attribute.from_dict(dict(type='domain', value=domain)) + attribute.from_dict(**dict(type='domain', value=domain)) self.misp_event.add_attribute(**attribute) return attribute.uuid From 6fdfcb0a29d8a88f3a9f60fa409ec7f35c4d37c2 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 22 Jul 2019 09:53:19 +0200 Subject: [PATCH 185/207] fix: Changed function name to avoid confusion with the same variable name --- misp_modules/modules/expansion/virustotal_public.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/misp_modules/modules/expansion/virustotal_public.py b/misp_modules/modules/expansion/virustotal_public.py index 12c73d5..1183c06 100644 --- a/misp_modules/modules/expansion/virustotal_public.py +++ b/misp_modules/modules/expansion/virustotal_public.py @@ -62,7 +62,7 @@ class VirusTotalParser(): vt_object.add_attribute('detection-ratio', type='text', value=detection_ratio) self.misp_event.add_object(**vt_object) - def query_result(self, query_type): + def get_query_result(self, query_type): params = {query_type: self.attribute.value, 'apikey': self.apikey} return requests.get(self.base_url, params=params) @@ -174,7 +174,7 @@ def handler(q=False): attribute = request['attribute'] query_type, to_call = misp_type_mapping[attribute['type']] parser = to_call(request['config']['apikey'], attribute) - query_result = parser.query_result(query_type) + query_result = parser.get_query_result(query_type) status_code = query_result.status_code if status_code == 200: parser.parse_report(query_result.json()) From c9c2027a57bbb823f37c89c9ed769d98ee50dd95 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 22 Jul 2019 11:39:46 +0200 Subject: [PATCH 186/207] fix: Undetected urls are represented in lists --- misp_modules/modules/expansion/virustotal_public.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/virustotal_public.py b/misp_modules/modules/expansion/virustotal_public.py index 1183c06..f95b8e4 100644 --- a/misp_modules/modules/expansion/virustotal_public.py +++ b/misp_modules/modules/expansion/virustotal_public.py @@ -31,7 +31,7 @@ class VirusTotalParser(): for feature in ('detected_urls', 'undetected_urls'): if feature in query_result: for url in query_result[feature]: - value = url['url'] if isinstance(url, dict) else url + value = url['url'] if isinstance(url, dict) else url[0] self.misp_event.add_attribute('url', value) def parse_resolutions(self, resolutions, subdomains=None, uuids=None): From 675e0815ff4e679a8660d2ced3990e8fe2fc3ea3 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 22 Jul 2019 11:42:52 +0200 Subject: [PATCH 187/207] add: Parsing communicating samples returned by domain reports --- misp_modules/modules/expansion/virustotal_public.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/virustotal_public.py b/misp_modules/modules/expansion/virustotal_public.py index f95b8e4..ed7fd0e 100644 --- a/misp_modules/modules/expansion/virustotal_public.py +++ b/misp_modules/modules/expansion/virustotal_public.py @@ -76,7 +76,7 @@ class DomainQuery(VirusTotalParser): def parse_report(self, query_result): hash_type = 'sha256' whois = 'whois' - for feature_type in ('referrer', 'downloaded'): + for feature_type in ('referrer', 'downloaded', 'communicating'): for feature in ('undetected_{}_samples', 'detected_{}_samples'): for sample in query_result[feature.format(feature_type)]: self.misp_event.add_attribute(hash_type, sample[hash_type]) From 1fa37ea712907817ad2546c79c75e3accc422b9a Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 22 Jul 2019 11:43:35 +0200 Subject: [PATCH 188/207] fix: Avoiding issues with non existing sample types --- misp_modules/modules/expansion/virustotal_public.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/virustotal_public.py b/misp_modules/modules/expansion/virustotal_public.py index ed7fd0e..a614a8c 100644 --- a/misp_modules/modules/expansion/virustotal_public.py +++ b/misp_modules/modules/expansion/virustotal_public.py @@ -78,7 +78,7 @@ class DomainQuery(VirusTotalParser): whois = 'whois' for feature_type in ('referrer', 'downloaded', 'communicating'): for feature in ('undetected_{}_samples', 'detected_{}_samples'): - for sample in query_result[feature.format(feature_type)]: + for sample in query_result.get(feature.format(feature_type), []): self.misp_event.add_attribute(hash_type, sample[hash_type]) if query_result.get(whois): whois_object = MISPObject(whois) From 14cf39d8b6ce58fbe769137dd8dc9e7065be66e3 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Mon, 22 Jul 2019 16:22:29 +0200 Subject: [PATCH 189/207] chg: Updated the module to work with the updated VirusTotal API - Parsing functions updated to support the updated format of the VirusTotal API responses - The module can now return objects - /!\ This module requires a high number of requests limit rate to work as expected /!\ --- misp_modules/modules/expansion/virustotal.py | 309 +++++++++++-------- 1 file changed, 174 insertions(+), 135 deletions(-) diff --git a/misp_modules/modules/expansion/virustotal.py b/misp_modules/modules/expansion/virustotal.py index 65623fb..1839bb3 100644 --- a/misp_modules/modules/expansion/virustotal.py +++ b/misp_modules/modules/expansion/virustotal.py @@ -1,167 +1,206 @@ +from pymisp import MISPAttribute, MISPEvent, MISPObject import json import requests -from requests import HTTPError -import base64 -from collections import defaultdict misperrors = {'error': 'Error'} -mispattributes = {'input': ['hostname', 'domain', "ip-src", "ip-dst", "md5", "sha1", "sha256", "sha512"], - 'output': ['domain', "ip-src", "ip-dst", "text", "md5", "sha1", "sha256", "sha512", "ssdeep", - "authentihash", "filename"]} +mispattributes = {'input': ['hostname', 'domain', "ip-src", "ip-dst", "md5", "sha1", "sha256", "sha512", "url"], + 'format': 'misp_standard'} # possible module-types: 'expansion', 'hover' or both -moduleinfo = {'version': '3', 'author': 'Hannah Ward', +moduleinfo = {'version': '4', 'author': 'Hannah Ward', 'description': 'Get information from virustotal', 'module-type': ['expansion']} # config fields that your code expects from the site admin -moduleconfig = ["apikey", "event_limit"] -comment = '{}: Enriched via VirusTotal' -hash_types = ["md5", "sha1", "sha256", "sha512"] +moduleconfig = ["apikey"] -class VirusTotalRequest(object): - def __init__(self, config): - self.apikey = config['apikey'] - self.limit = int(config.get('event_limit', 5)) +class VirusTotalParser(object): + def __init__(self, apikey): + self.apikey = apikey self.base_url = "https://www.virustotal.com/vtapi/v2/{}/report" - self.results = defaultdict(set) - self.to_return = [] - self.input_types_mapping = {'ip-src': self.get_ip, 'ip-dst': self.get_ip, - 'domain': self.get_domain, 'hostname': self.get_domain, - 'md5': self.get_hash, 'sha1': self.get_hash, - 'sha256': self.get_hash, 'sha512': self.get_hash} - self.output_types_mapping = {'submission_names': 'filename', 'ssdeep': 'ssdeep', - 'authentihash': 'authentihash', 'ITW_urls': 'url'} + self.misp_event = MISPEvent() + self.parsed_objects = {} + self.input_types_mapping = {'ip-src': self.parse_ip, 'ip-dst': self.parse_ip, + 'domain': self.parse_domain, 'hostname': self.parse_domain, + 'md5': self.parse_hash, 'sha1': self.parse_hash, + 'sha256': self.parse_hash, 'sha512': self.parse_hash, + 'url': self.parse_url} - def parse_request(self, q): - req_values = set() - for attribute_type, attribute_value in q.items(): - req_values.add(attribute_value) - try: - error = self.input_types_mapping[attribute_type](attribute_value) - except KeyError: - continue - if error is not None: - return error - for key, values in self.results.items(): - values = values.difference(req_values) - if values: - if isinstance(key, tuple): - types, comment = key - self.to_return.append({'types': list(types), 'values': list(values), 'comment': comment}) - else: - self.to_return.append({'types': key, 'values': list(values)}) - return self.to_return + def query_api(self, attribute): + self.attribute = MISPAttribute() + self.attribute.from_dict(**attribute) + return self.input_types_mapping[self.attribute.type](self.attribute.value, recurse=True) - def get_domain(self, domain, do_not_recurse=False): - req = requests.get(self.base_url.format('domain'), params={'domain': domain, 'apikey': self.apikey}) - try: - req.raise_for_status() + def get_result(self): + event = json.loads(self.misp_event.to_json())['Event'] + results = {key: event[key] for key in ('Attribute', 'Object') if (key in event and event[key])} + return {'results': results} + + ################################################################################ + #### Main parsing functions #### + ################################################################################ + + def parse_domain(self, domain, recurse=False): + req = requests.get(self.base_url.format('domain'), params={'apikey': self.apikey, 'domain': domain}) + if req.status_code != 200: + return req.status_code + req = req.json() + hash_type = 'sha256' + whois = 'whois' + feature_types = {'communicating': 'communicates-with', + 'downloaded': 'downloaded-from', + 'referrer': 'referring'} + siblings = (self.parse_siblings(domain) for domain in req['domain_siblings']) + uuid = self.parse_resolutions(req['resolutions'], req['subdomains'], siblings) + for feature_type, relationship in feature_types.items(): + for feature in ('undetected_{}_samples', 'detected_{}_samples'): + for sample in req.get(feature.format(feature_type), []): + status_code = self.parse_hash(sample[hash_type], False, uuid, relationship) + if status_code != 200: + return status_code + if req.get(whois): + whois_object = MISPObject(whois) + whois_object.add_attribute('text', type='text', value=req[whois]) + self.misp_event.add_object(**whois_object) + return self.parse_related_urls(req, recurse, uuid) + + def parse_hash(self, sample, recurse=False, uuid=None, relationship=None): + req = requests.get(self.base_url.format('file'), params={'apikey': self.apikey, 'resource': sample}) + status_code = req.status_code + if req.status_code == 200: req = req.json() - except HTTPError as e: - return str(e) - if req["response_code"] == 0: - # Nothing found - return [] - if "resolutions" in req: - for res in req["resolutions"][:self.limit]: - ip_address = res["ip_address"] - self.results[(("ip-dst", "ip-src"), comment.format(domain))].add(ip_address) - # Pivot from here to find all domain info - if not do_not_recurse: - error = self.get_ip(ip_address, True) - if error is not None: - return error - self.get_more_info(req) + vt_uuid = self.parse_vt_object(req) + file_attributes = [] + for hash_type in ('md5', 'sha1', 'sha256'): + if req.get(hash_type): + file_attributes.append({'type': hash_type, 'object_relation': hash_type, + 'value': req[hash_type]}) + if file_attributes: + file_object = MISPObject('file') + for attribute in file_attributes: + file_object.add_attribute(**attribute) + file_object.add_reference(vt_uuid, 'analyzed-with') + if uuid and relationship: + file_object.add_reference(uuid, relationship) + self.misp_event.add_object(**file_object) + return status_code - def get_hash(self, _hash): - req = requests.get(self.base_url.format('file'), params={'resource': _hash, 'apikey': self.apikey, 'allinfo': 1}) - try: - req.raise_for_status() + def parse_ip(self, ip, recurse=False): + req = requests.get(self.base_url.format('ip-address'), params={'apikey': self.apikey, 'ip': ip}) + if req.status_code != 200: + return req.status_code + req = req.json() + if req.get('asn'): + asn_mapping = {'network': ('ip-src', 'subnet-announced'), + 'country': ('text', 'country')} + asn_object = MISPObject('asn') + asn_object.add_attribute('asn', type='AS', value=req['asn']) + for key, value in asn_mapping.items(): + if req.get(key): + attribute_type, relation = value + asn_object.add_attribute(relation, type=attribute_type, value=req[key]) + self.misp_event.add_object(**asn_object) + uuid = self.parse_resolutions(req['resolutions']) if req.get('resolutions') else None + return self.parse_related_urls(req, recurse, uuid) + + def parse_url(self, url, recurse=False, uuid=None): + req = requests.get(self.base_url.format('url'), params={'apikey': self.apikey, 'resource': url}) + status_code = req.status_code + if req.status_code == 200: req = req.json() - except HTTPError as e: - return str(e) - if req["response_code"] == 0: - # Nothing found - return [] - self.get_more_info(req) + vt_uuid = self.parse_vt_object(req) + if not recurse: + feature = 'url' + url_object = MISPObject(feature) + url_object.add_attribute(feature, type=feature, value=url) + url_object.add_reference(vt_uuid, 'analyzed-with') + if uuid: + url_object.add_reference(uuid, 'hosted-in') + self.misp_event.add_object(**url_object) + return status_code - def get_ip(self, ip, do_not_recurse=False): - req = requests.get(self.base_url.format('ip-address'), params={'ip': ip, 'apikey': self.apikey}) - try: - req.raise_for_status() - req = req.json() - except HTTPError as e: - return str(e) - if req["response_code"] == 0: - # Nothing found - return [] - if "resolutions" in req: - for res in req["resolutions"][:self.limit]: - hostname = res["hostname"] - self.results[(("domain",), comment.format(ip))].add(hostname) - # Pivot from here to find all domain info - if not do_not_recurse: - error = self.get_domain(hostname, True) - if error is not None: - return error - self.get_more_info(req) + ################################################################################ + #### Additional parsing functions #### + ################################################################################ - def find_all(self, data): - hashes = [] - if isinstance(data, dict): - for key, value in data.items(): - if key in hash_types: - self.results[key].add(value) - hashes.append(value) - else: - if isinstance(value, (dict, list)): - hashes.extend(self.find_all(value)) - elif isinstance(data, list): - for d in data: - hashes.extend(self.find_all(d)) - return hashes + def parse_related_urls(self, query_result, recurse, uuid=None): + if recurse: + for feature in ('detected_urls', 'undetected_urls'): + if feature in query_result: + for url in query_result[feature]: + value = url['url'] if isinstance(url, dict) else url[0] + status_code = self.parse_url(value, False, uuid) + if status_code != 200: + return status_code + else: + for feature in ('detected_urls', 'undetected_urls'): + if feature in query_result: + for url in query_result[feature]: + value = url['url'] if isinstance(url, dict) else url[0] + self.misp_event.add_attribute('url', value) + return 200 - def get_more_info(self, req): - # Get all hashes first - hashes = self.find_all(req) - for h in hashes[:self.limit]: - # Search VT for some juicy info - try: - data = requests.get(self.base_url.format('file'), params={'resource': h, 'apikey': self.apikey, 'allinfo': 1}).json() - except Exception: - continue - # Go through euch key and check if it exists - for VT_type, MISP_type in self.output_types_mapping.items(): - if VT_type in data: - try: - self.results[((MISP_type,), comment.format(h))].add(data[VT_type]) - except TypeError: - self.results[((MISP_type,), comment.format(h))].update(data[VT_type]) - # Get the malware sample - sample = requests.get(self.base_url[:-6].format('file/download'), params={'hash': h, 'apikey': self.apikey}) - malsample = sample.content - # It is possible for VT to not give us any submission names - if "submission_names" in data: - self.to_return.append({"types": ["malware-sample"], "categories": ["Payload delivery"], - "values": data["submimssion_names"], "data": str(base64.b64encore(malsample), 'utf-8')}) + def parse_resolutions(self, resolutions, subdomains=None, uuids=None): + domain_ip_object = MISPObject('domain-ip') + if self.attribute.type == 'domain': + domain_ip_object.add_attribute('domain', type='domain', value=self.attribute.value) + attribute_type, relation, key = ('ip-dst', 'ip', 'ip_address') + else: + domain_ip_object.add_attribute('ip', type='ip-dst', value=self.attribute.value) + attribute_type, relation, key = ('domain', 'domain', 'hostname') + for resolution in resolutions: + domain_ip_object.add_attribute(relation, type=attribute_type, value=resolution[key]) + if subdomains: + for subdomain in subdomains: + attribute = MISPAttribute() + attribute.from_dict(**dict(type='domain', value=subdomain)) + self.misp_event.add_attribute(**attribute) + domain_ip_object.add_reference(attribute.uuid, 'subdomain') + if uuids: + for uuid in uuids: + domain_ip_object.add_reference(uuid, 'sibling-of') + self.misp_event.add_object(**domain_ip_object) + return domain_ip_object.uuid + + def parse_siblings(self, domain): + attribute = MISPAttribute() + attribute.from_dict(**dict(type='domain', value=domain)) + self.misp_event.add_attribute(**attribute) + return attribute.uuid + + def parse_vt_object(self, query_result): + vt_object = MISPObject('virustotal-report') + vt_object.add_attribute('permalink', type='link', value=query_result['permalink']) + detection_ratio = '{}/{}'.format(query_result['positives'], query_result['total']) + vt_object.add_attribute('detection-ratio', type='text', value=detection_ratio) + self.misp_event.add_object(**vt_object) + return vt_object.uuid + + +def parse_error(status_code): + status_mapping = {204: 'VirusTotal request rate limit exceeded.', + 400: 'Incorrect request, please check the arguments.', + 403: 'You don\'t have enough privileges to make the request.'} + if status_code in status_mapping: + return status_mapping[status_code] + return "VirusTotal may not be accessible." def handler(q=False): if q is False: return False - q = json.loads(q) - if not q.get('config') or not q['config'].get('apikey'): + request = json.loads(q) + if not request.get('config') or not request['config'].get('apikey'): misperrors['error'] = "A VirusTotal api key is required for this module." return misperrors - del q['module'] - query = VirusTotalRequest(q.pop('config')) - r = query.parse_request(q) - if isinstance(r, str): - misperrors['error'] = r + parser = VirusTotalParser(request['config']['apikey']) + attribute = request['attribute'] + status = parser.query_api(attribute) + if status != 200: + misperrors['error'] = parse_error(status) return misperrors - return {'results': r} + return parser.get_result() def introspection(): From 13d683f7c683c96f2119422714ad65c0ddf878e9 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Tue, 23 Jul 2019 09:31:06 +0200 Subject: [PATCH 190/207] add: [documentation] Updated README and documentation with the virustotal modules changes --- README.md | 3 ++- doc/README.md | 38 +++++++++++++++++++++++----- doc/expansion/virustotal.json | 10 ++++---- doc/expansion/virustotal_public.json | 9 +++++++ 4 files changed, 47 insertions(+), 13 deletions(-) create mode 100644 doc/expansion/virustotal_public.json diff --git a/README.md b/README.md index c9fd915..bd998a8 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,8 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ * [threatminer](misp_modules/modules/expansion/threatminer.py) - an expansion module to expand from [ThreatMiner](https://www.threatminer.org/). * [urlhaus](misp_modules/modules/expansion/urlhaus.py) - Query urlhaus to get additional data about a domain, hash, hostname, ip or url. * [urlscan](misp_modules/modules/expansion/urlscan.py) - an expansion module to query [urlscan.io](https://urlscan.io). -* [virustotal](misp_modules/modules/expansion/virustotal.py) - an expansion module to pull known resolutions and malware samples related with an IP/Domain from virusTotal (this modules require a VirusTotal private API key) +* [virustotal](misp_modules/modules/expansion/virustotal.py) - an expansion module to query the [VirusTotal](https://www.virustotal.com/gui/home) API with a high request rate limit required. (More details about the API: [here](https://developers.virustotal.com/reference)) +* [virustotal_public](misp_modules/modules/expansion/virustotal_public.py) - an expansion module to query the [VirusTotal](https://www.virustotal.com/gui/home) API with a public key and a low request rate limit. (More details about the API: [here](https://developers.virustotal.com/reference)) * [VMray](misp_modules/modules/expansion/vmray_submit.py) - a module to submit a sample to VMray. * [VulnDB](misp_modules/modules/expansion/vulndb.py) - a module to query [VulnDB](https://www.riskbasedsecurity.com/). * [Vulners](misp_modules/modules/expansion/vulners.py) - an expansion module to expand information about CVEs using Vulners API. diff --git a/doc/README.md b/doc/README.md index fd5b73b..d5d2ed0 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1042,21 +1042,45 @@ An expansion module to query urlscan.io. -Module to get information from virustotal. +Module to get advanced information from virustotal. - **features**: ->This module takes a MISP attribute as input and queries the VirusTotal API with it, in order to get additional data on the input attribute. +>New format of modules able to return attributes and objects. > ->Multiple recursive requests on the API can then be processed on some attributes found in the first request. A limit can be set to restrict the number of values to query again, and at the same time the number of request submitted to the API. +>A module to take a MISP attribute as input and query the VirusTotal API to get additional data about it. > ->This limit is important because the default user VirusTotal apikey only allows to process a certain nunmber of queries per minute. As a consequence it is recommended to have a larger number of requests or a private apikey. +>Compared to the [standard VirusTotal expansion module](https://github.com/MISP/misp-modules/blob/master/misp_modules/modules/expansion/virustotal.py), this module is made for advanced parsing of VirusTotal report, with a recursive analysis of the elements found after the first request. > ->Data is then mapped into MISP attributes. +>Thus, it requires a higher request rate limit to avoid the API to return a 204 error (Request rate limit exceeded), and the data parsed from the different requests are returned as MISP attributes and objects, with the corresponding relations between each one of them. - **input**: >A domain, hash (md5, sha1, sha256 or sha512), hostname or IP address attribute. - **output**: ->MISP attributes mapped from the rersult of the query on VirusTotal API. +>MISP attributes and objects resulting from the parsing of the VirusTotal report concerning the input attribute. - **references**: ->https://www.virustotal.com/ +>https://www.virustotal.com/, https://developers.virustotal.com/reference +- **requirements**: +>An access to the VirusTotal API (apikey), with a high request rate limit. + +----- + +#### [virustotal_public](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/virustotal_public.py) + + + +Module to get information from VirusTotal. +- **features**: +>New format of modules able to return attributes and objects. +> +>A module to take a MISP attribute as input and query the VirusTotal API to get additional data about it. +> +>Compared to the [more advanced VirusTotal expansion module](https://github.com/MISP/misp-modules/blob/master/misp_modules/modules/expansion/virustotal.py), this module is made for VirusTotal users who have a low request rate limit. +> +>Thus, it only queries the API once and returns the results that is parsed into MISP attributes and objects. +- **input**: +>A domain, hostname, ip, url or hash (md5, sha1, sha256 or sha512) attribute. +- **output**: +>MISP attributes and objects resulting from the parsing of the VirusTotal report concerning the input attribute. +- **references**: +>https://www.virustotal.com, https://developers.virustotal.com/reference - **requirements**: >An access to the VirusTotal API (apikey) diff --git a/doc/expansion/virustotal.json b/doc/expansion/virustotal.json index 9008003..060069e 100644 --- a/doc/expansion/virustotal.json +++ b/doc/expansion/virustotal.json @@ -1,9 +1,9 @@ { - "description": "Module to get information from virustotal.", + "description": "Module to get advanced information from virustotal.", "logo": "logos/virustotal.png", - "requirements": ["An access to the VirusTotal API (apikey)"], + "requirements": ["An access to the VirusTotal API (apikey), with a high request rate limit."], "input": "A domain, hash (md5, sha1, sha256 or sha512), hostname or IP address attribute.", - "output": "MISP attributes mapped from the rersult of the query on VirusTotal API.", - "references": ["https://www.virustotal.com/"], - "features": "This module takes a MISP attribute as input and queries the VirusTotal API with it, in order to get additional data on the input attribute.\n\nMultiple recursive requests on the API can then be processed on some attributes found in the first request. A limit can be set to restrict the number of values to query again, and at the same time the number of request submitted to the API.\n\nThis limit is important because the default user VirusTotal apikey only allows to process a certain nunmber of queries per minute. As a consequence it is recommended to have a larger number of requests or a private apikey.\n\nData is then mapped into MISP attributes." + "output": "MISP attributes and objects resulting from the parsing of the VirusTotal report concerning the input attribute.", + "references": ["https://www.virustotal.com/", "https://developers.virustotal.com/reference"], + "features": "New format of modules able to return attributes and objects.\n\nA module to take a MISP attribute as input and query the VirusTotal API to get additional data about it.\n\nCompared to the [standard VirusTotal expansion module](https://github.com/MISP/misp-modules/blob/master/misp_modules/modules/expansion/virustotal.py), this module is made for advanced parsing of VirusTotal report, with a recursive analysis of the elements found after the first request.\n\nThus, it requires a higher request rate limit to avoid the API to return a 204 error (Request rate limit exceeded), and the data parsed from the different requests are returned as MISP attributes and objects, with the corresponding relations between each one of them." } diff --git a/doc/expansion/virustotal_public.json b/doc/expansion/virustotal_public.json new file mode 100644 index 0000000..242c734 --- /dev/null +++ b/doc/expansion/virustotal_public.json @@ -0,0 +1,9 @@ +{ + "description": "Module to get information from VirusTotal.", + "logo": "logos/virustotal.png", + "requirements": ["An access to the VirusTotal API (apikey)"], + "input": "A domain, hostname, ip, url or hash (md5, sha1, sha256 or sha512) attribute.", + "output": "MISP attributes and objects resulting from the parsing of the VirusTotal report concerning the input attribute.", + "references": ["https://www.virustotal.com", "https://developers.virustotal.com/reference"], + "features": "New format of modules able to return attributes and objects.\n\nA module to take a MISP attribute as input and query the VirusTotal API to get additional data about it.\n\nCompared to the [more advanced VirusTotal expansion module](https://github.com/MISP/misp-modules/blob/master/misp_modules/modules/expansion/virustotal.py), this module is made for VirusTotal users who have a low request rate limit.\n\nThus, it only queries the API once and returns the results that is parsed into MISP attributes and objects." +} From 3e5b829bc55e1dcc0b63c6279d948e90372e4226 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Tue, 23 Jul 2019 09:35:22 +0200 Subject: [PATCH 191/207] fix: Fixed link in documentation --- doc/README.md | 2 +- doc/expansion/virustotal.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/README.md b/doc/README.md index d5d2ed0..0dc12af 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1048,7 +1048,7 @@ Module to get advanced information from virustotal. > >A module to take a MISP attribute as input and query the VirusTotal API to get additional data about it. > ->Compared to the [standard VirusTotal expansion module](https://github.com/MISP/misp-modules/blob/master/misp_modules/modules/expansion/virustotal.py), this module is made for advanced parsing of VirusTotal report, with a recursive analysis of the elements found after the first request. +>Compared to the [standard VirusTotal expansion module](https://github.com/MISP/misp-modules/blob/master/misp_modules/modules/expansion/virustotal_public.py), this module is made for advanced parsing of VirusTotal report, with a recursive analysis of the elements found after the first request. > >Thus, it requires a higher request rate limit to avoid the API to return a 204 error (Request rate limit exceeded), and the data parsed from the different requests are returned as MISP attributes and objects, with the corresponding relations between each one of them. - **input**: diff --git a/doc/expansion/virustotal.json b/doc/expansion/virustotal.json index 060069e..31fd6ac 100644 --- a/doc/expansion/virustotal.json +++ b/doc/expansion/virustotal.json @@ -5,5 +5,5 @@ "input": "A domain, hash (md5, sha1, sha256 or sha512), hostname or IP address attribute.", "output": "MISP attributes and objects resulting from the parsing of the VirusTotal report concerning the input attribute.", "references": ["https://www.virustotal.com/", "https://developers.virustotal.com/reference"], - "features": "New format of modules able to return attributes and objects.\n\nA module to take a MISP attribute as input and query the VirusTotal API to get additional data about it.\n\nCompared to the [standard VirusTotal expansion module](https://github.com/MISP/misp-modules/blob/master/misp_modules/modules/expansion/virustotal.py), this module is made for advanced parsing of VirusTotal report, with a recursive analysis of the elements found after the first request.\n\nThus, it requires a higher request rate limit to avoid the API to return a 204 error (Request rate limit exceeded), and the data parsed from the different requests are returned as MISP attributes and objects, with the corresponding relations between each one of them." + "features": "New format of modules able to return attributes and objects.\n\nA module to take a MISP attribute as input and query the VirusTotal API to get additional data about it.\n\nCompared to the [standard VirusTotal expansion module](https://github.com/MISP/misp-modules/blob/master/misp_modules/modules/expansion/virustotal_public.py), this module is made for advanced parsing of VirusTotal report, with a recursive analysis of the elements found after the first request.\n\nThus, it requires a higher request rate limit to avoid the API to return a 204 error (Request rate limit exceeded), and the data parsed from the different requests are returned as MISP attributes and objects, with the corresponding relations between each one of them." } From 92d90e8e1ca21db892759c3c0759efdd6a338266 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Tue, 23 Jul 2019 09:42:10 +0200 Subject: [PATCH 192/207] add: TODO comment for the next improvement --- misp_modules/modules/expansion/virustotal.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/misp_modules/modules/expansion/virustotal.py b/misp_modules/modules/expansion/virustotal.py index 1839bb3..d962691 100644 --- a/misp_modules/modules/expansion/virustotal.py +++ b/misp_modules/modules/expansion/virustotal.py @@ -15,6 +15,8 @@ moduleinfo = {'version': '4', 'author': 'Hannah Ward', moduleconfig = ["apikey"] +# TODO: Parse the report with a private API key to be able to get more advanced results from a query with 'allinfo' set to True + class VirusTotalParser(object): def __init__(self, apikey): self.apikey = apikey From 79992f020461d2ac4da9ce19566a36c18f4962e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Wed, 24 Jul 2019 09:24:46 +0200 Subject: [PATCH 193/207] chg: Bump dependencies --- Pipfile | 2 +- Pipfile.lock | 451 +++++++++++++++++++++------------------ tests/test_expansions.py | 2 +- 3 files changed, 244 insertions(+), 211 deletions(-) diff --git a/Pipfile b/Pipfile index 9856955..041c273 100644 --- a/Pipfile +++ b/Pipfile @@ -54,7 +54,7 @@ pandas_ods_reader = "*" pdftotext = "*" lxml = "*" xlrd = "*" -idna-ssl = {markers="python_version < '3.7'"} +idna-ssl = {markers = "python_version < '3.7'"} jbxapi = "*" [requires] diff --git a/Pipfile.lock b/Pipfile.lock index 5570331..116fb4e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -74,12 +74,12 @@ }, "beautifulsoup4": { "hashes": [ - "sha256:034740f6cb549b4e932ae1ab975581e6103ac8f942200a0e9759065984391858", - "sha256:945065979fb8529dd2f37dbb58f00b661bdbcbebf954f93b32fdf5263ef35348", - "sha256:ba6d5c59906a85ac23dadfe5c88deaf3e179ef565f4898671253e50a78680718" + "sha256:05668158c7b85b791c5abde53e50265e16f98ad601c402ba44d70f96c4159612", + "sha256:25288c9e176f354bf277c0a10aa96c782a6a18a17122dba2e8cec4a97e03343b", + "sha256:f040590be10520f2ea4c2ae8c3dae441c7cfff5308ec9d58a0ec0c1b8f81d469" ], "index": "pypi", - "version": "==4.7.1" + "version": "==4.8.0" }, "blockchain": { "hashes": [ @@ -90,10 +90,10 @@ }, "certifi": { "hashes": [ - "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", - "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae" + "sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", + "sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695" ], - "version": "==2019.3.9" + "version": "==2019.6.16" }, "chardet": { "hashes": [ @@ -123,6 +123,13 @@ ], "version": "==0.4.1" }, + "deprecated": { + "hashes": [ + "sha256:a515c4cf75061552e0284d123c3066fbbe398952c87333a92b8fc3dd8e4f9cc1", + "sha256:b07b414c8aac88f60c1d837d21def7e83ba711052e03b3cbaff27972567a8f8d" + ], + "version": "==1.2.6" + }, "dnspython": { "hashes": [ "sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01", @@ -165,10 +172,10 @@ }, "httplib2": { "hashes": [ - "sha256:23914b5487dfe8ef09db6656d6d63afb0cf3054ad9ebc50868ddc8e166b5f8e8", - "sha256:a18121c7c72a56689efbf1aef990139ad940fee1e64c6f2458831736cd593600" + "sha256:158fbd0ffbba536829d664bf3f32c4f45df41f8f791663665162dfaf21ffd075", + "sha256:d1146939d270f1f1eb8cbf8f5aa72ff37d897faccca448582bb1e180aeb4c6b2" ], - "version": "==0.12.3" + "version": "==0.13.0" }, "idna": { "hashes": [ @@ -194,10 +201,10 @@ }, "jbxapi": { "hashes": [ - "sha256:ff7c74b3cc06aebd3f2d99a1ffb042b842d527faff1d6006f6224907fcf6ce6f" + "sha256:b06d7dc99af51eff657b1bb5d96489dda6af6164fae934d9de8b00795a4bd5fd" ], "index": "pypi", - "version": "==3.1.3" + "version": "==3.2.0" }, "jsonschema": { "hashes": [ @@ -208,35 +215,33 @@ }, "lxml": { "hashes": [ - "sha256:03984196d00670b2ab14ae0ea83d5cc0cfa4f5a42558afa9ab5fa745995328f5", - "sha256:0815b0c9f897468de6a386dc15917a0becf48cc92425613aa8bbfc7f0f82951f", - "sha256:175f3825f075cf02d15099eb52658457cf0ff103dcf11512b5d2583e1d40f58b", - "sha256:30e14c62d88d1e01a26936ecd1c6e784d4afc9aa002bba4321c5897937112616", - "sha256:3210da6f36cf4b835ff1be853962b22cc354d506f493b67a4303c88bbb40d57b", - "sha256:40f60819fbd5bad6e191ba1329bfafa09ab7f3f174b3d034d413ef5266963294", - "sha256:43b26a865a61549919f8a42e094dfdb62847113cf776d84bd6b60e4e3fc20ea3", - "sha256:4a03dd682f8e35a10234904e0b9508d705ff98cf962c5851ed052e9340df3d90", - "sha256:62f382cddf3d2e52cf266e161aa522d54fd624b8cc567bc18f573d9d50d40e8e", - "sha256:7b98f0325be8450da70aa4a796c4f06852949fe031878b4aa1d6c417a412f314", - "sha256:846a0739e595871041385d86d12af4b6999f921359b38affb99cdd6b54219a8f", - "sha256:a3080470559938a09a5d0ec558c005282e99ac77bf8211fb7b9a5c66390acd8d", - "sha256:ad841b78a476623955da270ab8d207c3c694aa5eba71f4792f65926dc46c6ee8", - "sha256:afdd75d9735e44c639ffd6258ce04a2de3b208f148072c02478162d0944d9da3", - "sha256:b4fbf9b552faff54742bcd0791ab1da5863363fb19047e68f6592be1ac2dab33", - "sha256:b90c4e32d6ec089d3fa3518436bdf5ce4d902a0787dbd9bb09f37afe8b994317", - "sha256:b91cfe4438c741aeff662d413fd2808ac901cc6229c838236840d11de4586d63", - "sha256:bdb0593a42070b0a5f138b79b872289ee73c8e25b3f0bea6564e795b55b6bcdd", - "sha256:c4e4bca2bb68ce22320297dfa1a7bf070a5b20bcbaec4ee023f83d2f6e76496f", - "sha256:cec4ab14af9eae8501be3266ff50c3c2aecc017ba1e86c160209bb4f0423df6a", - "sha256:e83b4b2bf029f5104bc1227dbb7bf5ace6fd8fabaebffcd4f8106fafc69fc45f", - "sha256:e995b3734a46d41ae60b6097f7c51ba9958648c6d1e0935b7e0ee446ee4abe22", - "sha256:f679d93dec7f7210575c85379a31322df4c46496f184ef650d3aba1484b38a2d", - "sha256:fd213bb5166e46974f113c8228daaef1732abc47cb561ce9c4c8eaed4bd3b09b", - "sha256:fdcb57b906dbc1f80666e6290e794ab8fb959a2e17aa5aee1758a85d1da4533f", - "sha256:ff424b01d090ffe1947ec7432b07f536912e0300458f9a7f48ea217dd8362b86" + "sha256:06c7616601430aa140a69f97e3116308fffe0848f543b639a5ec2e8920ae72fd", + "sha256:177202792f9842374a8077735c69c41a4282183f7851443d2beb8ee310720819", + "sha256:19317ad721ceb9e39847d11131903931e2794e447d4751ebb0d9236f1b349ff2", + "sha256:36d206e62f3e5dbaafd4ec692b67157e271f5da7fd925fda8515da675eace50d", + "sha256:387115b066c797c85f9861a9613abf50046a15aac16759bc92d04f94acfad082", + "sha256:3ce1c49d4b4a7bc75fb12acb3a6247bb7a91fe420542e6d671ba9187d12a12c2", + "sha256:4d2a5a7d6b0dbb8c37dab66a8ce09a8761409c044017721c21718659fa3365a1", + "sha256:58d0a1b33364d1253a88d18df6c0b2676a1746d27c969dc9e32d143a3701dda5", + "sha256:62a651c618b846b88fdcae0533ec23f185bb322d6c1845733f3123e8980c1d1b", + "sha256:69ff21064e7debc9b1b1e2eee8c2d686d042d4257186d70b338206a80c5bc5ea", + "sha256:7060453eba9ba59d821625c6af6a266bd68277dce6577f754d1eb9116c094266", + "sha256:7d26b36a9c4bce53b9cfe42e67849ae3c5c23558bc08363e53ffd6d94f4ff4d2", + "sha256:83b427ad2bfa0b9705e02a83d8d607d2c2f01889eb138168e462a3a052c42368", + "sha256:923d03c84534078386cf50193057aae98fa94cace8ea7580b74754493fda73ad", + "sha256:b773715609649a1a180025213f67ffdeb5a4878c784293ada300ee95a1f3257b", + "sha256:baff149c174e9108d4a2fee192c496711be85534eab63adb122f93e70aa35431", + "sha256:bca9d118b1014b4c2d19319b10a3ebed508ff649396ce1855e1c96528d9b2fa9", + "sha256:ce580c28845581535dc6000fc7c35fdadf8bea7ccb57d6321b044508e9ba0685", + "sha256:d34923a569e70224d88e6682490e24c842907ba2c948c5fd26185413cbe0cd96", + "sha256:dd9f0e531a049d8b35ec5e6c68a37f1ba6ec3a591415e6804cbdf652793d15d7", + "sha256:ecb805cbfe9102f3fd3d2ef16dfe5ae9e2d7a7dfbba92f4ff1e16ac9784dbfb0", + "sha256:ede9aad2197a0202caff35d417b671f5f91a3631477441076082a17c94edd846", + "sha256:ef2d1fc370400e0aa755aab0b20cf4f1d0e934e7fd5244f3dd4869078e4942b9", + "sha256:f2fec194a49bfaef42a548ee657362af5c7a640da757f6f452a35da7dd9f923c" ], "index": "pypi", - "version": "==4.3.3" + "version": "==4.3.4" }, "maclookup": { "hashes": [ @@ -293,31 +298,31 @@ }, "numpy": { "hashes": [ - "sha256:0e2eed77804b2a6a88741f8fcac02c5499bba3953ec9c71e8b217fad4912c56c", - "sha256:1c666f04553ef70fda54adf097dbae7080645435fc273e2397f26bbf1d127bbb", - "sha256:1f46532afa7b2903bfb1b79becca2954c0a04389d19e03dc73f06b039048ac40", - "sha256:315fa1b1dfc16ae0f03f8fd1c55f23fd15368710f641d570236f3d78af55e340", - "sha256:3d5fcea4f5ed40c3280791d54da3ad2ecf896f4c87c877b113576b8280c59441", - "sha256:48241759b99d60aba63b0e590332c600fc4b46ad597c9b0a53f350b871ef0634", - "sha256:4b4f2924b36d857cf302aec369caac61e43500c17eeef0d7baacad1084c0ee84", - "sha256:54fe3b7ed9e7eb928bbc4318f954d133851865f062fa4bbb02ef8940bc67b5d2", - "sha256:5a8f021c70e6206c317974c93eaaf9bc2b56295b6b1cacccf88846e44a1f33fc", - "sha256:754a6be26d938e6ca91942804eb209307b73f806a1721176278a6038869a1686", - "sha256:771147e654e8b95eea1293174a94f34e2e77d5729ad44aefb62fbf8a79747a15", - "sha256:78a6f89da87eeb48014ec652a65c4ffde370c036d780a995edaeb121d3625621", - "sha256:7fde5c2a3a682a9e101e61d97696687ebdba47637611378b4127fe7e47fdf2bf", - "sha256:80d99399c97f646e873dd8ce87c38cfdbb668956bbc39bc1e6cac4b515bba2a0", - "sha256:88a72c1e45a0ae24d1f249a529d9f71fe82e6fa6a3fd61414b829396ec585900", - "sha256:a4f4460877a16ac73302a9c077ca545498d9fe64e6a81398d8e1a67e4695e3df", - "sha256:a61255a765b3ac73ee4b110b28fccfbf758c985677f526c2b4b39c48cc4b509d", - "sha256:ab4896a8c910b9a04c0142871d8800c76c8a2e5ff44763513e1dd9d9631ce897", - "sha256:abbd6b1c2ef6199f4b7ca9f818eb6b31f17b73a6110aadc4e4298c3f00fab24e", - "sha256:b16d88da290334e33ea992c56492326ea3b06233a00a1855414360b77ca72f26", - "sha256:b78a1defedb0e8f6ae1eb55fa6ac74ab42acc4569c3a2eacc2a407ee5d42ebcb", - "sha256:cfef82c43b8b29ca436560d51b2251d5117818a8d1fb74a8384a83c096745dad", - "sha256:d160e57731fcdec2beda807ebcabf39823c47e9409485b5a3a1db3a8c6ce763e" + "sha256:0778076e764e146d3078b17c24c4d89e0ecd4ac5401beff8e1c87879043a0633", + "sha256:141c7102f20abe6cf0d54c4ced8d565b86df4d3077ba2343b61a6db996cefec7", + "sha256:14270a1ee8917d11e7753fb54fc7ffd1934f4d529235beec0b275e2ccf00333b", + "sha256:27e11c7a8ec9d5838bc59f809bfa86efc8a4fd02e58960fa9c49d998e14332d5", + "sha256:2a04dda79606f3d2f760384c38ccd3d5b9bb79d4c8126b67aff5eb09a253763e", + "sha256:3c26010c1b51e1224a3ca6b8df807de6e95128b0908c7e34f190e7775455b0ca", + "sha256:52c40f1a4262c896420c6ea1c6fda62cf67070e3947e3307f5562bd783a90336", + "sha256:6e4f8d9e8aa79321657079b9ac03f3cf3fd067bf31c1cca4f56d49543f4356a5", + "sha256:7242be12a58fec245ee9734e625964b97cf7e3f2f7d016603f9e56660ce479c7", + "sha256:7dc253b542bfd4b4eb88d9dbae4ca079e7bf2e2afd819ee18891a43db66c60c7", + "sha256:94f5bd885f67bbb25c82d80184abbf7ce4f6c3c3a41fbaa4182f034bba803e69", + "sha256:a89e188daa119ffa0d03ce5123dee3f8ffd5115c896c2a9d4f0dbb3d8b95bfa3", + "sha256:ad3399da9b0ca36e2f24de72f67ab2854a62e623274607e37e0ce5f5d5fa9166", + "sha256:b0348be89275fd1d4c44ffa39530c41a21062f52299b1e3ee7d1c61f060044b8", + "sha256:b5554368e4ede1856121b0dfa35ce71768102e4aa55e526cb8de7f374ff78722", + "sha256:cbddc56b2502d3f87fda4f98d948eb5b11f36ff3902e17cb6cc44727f2200525", + "sha256:d79f18f41751725c56eceab2a886f021d70fd70a6188fd386e29a045945ffc10", + "sha256:dc2ca26a19ab32dc475dbad9dfe723d3a64c835f4c23f625c2b6566ca32b9f29", + "sha256:dd9bcd4f294eb0633bb33d1a74febdd2b9018b8b8ed325f861fffcd2c7660bb8", + "sha256:e8baab1bc7c9152715844f1faca6744f2416929de10d7639ed49555a85549f52", + "sha256:ec31fe12668af687b99acf1567399632a7c47b0e17cfb9ae47c098644ef36797", + "sha256:f12b4f7e2d8f9da3141564e6737d79016fe5336cc92de6814eba579744f65b0a", + "sha256:f58ac38d5ca045a377b3b377c84df8175ab992c970a53332fa8ac2373df44ff7" ], - "version": "==1.16.3" + "version": "==1.16.4" }, "oauth2": { "hashes": [ @@ -367,44 +372,40 @@ }, "pandas": { "hashes": [ - "sha256:071e42b89b57baa17031af8c6b6bbd2e9a5c68c595bc6bf9adabd7a9ed125d3b", - "sha256:17450e25ae69e2e6b303817bdf26b2cd57f69595d8550a77c308be0cd0fd58fa", - "sha256:17916d818592c9ec891cbef2e90f98cc85e0f1e89ed0924c9b5220dc3209c846", - "sha256:2538f099ab0e9f9c9d09bbcd94b47fd889bad06dc7ae96b1ed583f1dc1a7a822", - "sha256:366f30710172cb45a6b4f43b66c220653b1ea50303fbbd94e50571637ffb9167", - "sha256:42e5ad741a0d09232efbc7fc648226ed93306551772fc8aecc6dce9f0e676794", - "sha256:4e718e7f395ba5bfe8b6f6aaf2ff1c65a09bb77a36af6394621434e7cc813204", - "sha256:4f919f409c433577a501e023943e582c57355d50a724c589e78bc1d551a535a2", - "sha256:4fe0d7e6438212e839fc5010c78b822664f1a824c0d263fd858f44131d9166e2", - "sha256:5149a6db3e74f23dc3f5a216c2c9ae2e12920aa2d4a5b77e44e5b804a5f93248", - "sha256:627594338d6dd995cfc0bacd8e654cd9e1252d2a7c959449228df6740d737eb8", - "sha256:83c702615052f2a0a7fb1dd289726e29ec87a27272d775cb77affe749cca28f8", - "sha256:8c872f7fdf3018b7891e1e3e86c55b190e6c5cee70cab771e8f246c855001296", - "sha256:90f116086063934afd51e61a802a943826d2aac572b2f7d55caaac51c13db5b5", - "sha256:a3352bacac12e1fc646213b998bce586f965c9d431773d9e91db27c7c48a1f7d", - "sha256:bcdd06007cca02d51350f96debe51331dec429ac8f93930a43eb8fb5639e3eb5", - "sha256:c1bd07ebc15285535f61ddd8c0c75d0d6293e80e1ee6d9a8d73f3f36954342d0", - "sha256:c9a4b7c55115eb278c19aa14b34fcf5920c8fe7797a09b7b053ddd6195ea89b3", - "sha256:cc8fc0c7a8d5951dc738f1c1447f71c43734244453616f32b8aa0ef6013a5dfb", - "sha256:d7b460bc316064540ce0c41c1438c416a40746fd8a4fb2999668bf18f3c4acf1" + "sha256:074a032f99bb55d178b93bd98999c971542f19317829af08c99504febd9e9b8b", + "sha256:20f1728182b49575c2f6f681b3e2af5fac9e84abdf29488e76d569a7969b362e", + "sha256:2745ba6e16c34d13d765c3657bb64fa20a0e2daf503e6216a36ed61770066179", + "sha256:32c44e5b628c48ba17703f734d59f369d4cdcb4239ef26047d6c8a8bfda29a6b", + "sha256:3b9f7dcee6744d9dcdd53bce19b91d20b4311bf904303fa00ef58e7df398e901", + "sha256:544f2033250980fb6f069ce4a960e5f64d99b8165d01dc39afd0b244eeeef7d7", + "sha256:58f9ef68975b9f00ba96755d5702afdf039dea9acef6a0cfd8ddcde32918a79c", + "sha256:9023972a92073a495eba1380824b197ad1737550fe1c4ef8322e65fe58662888", + "sha256:914341ad2d5b1ea522798efa4016430b66107d05781dbfe7cf05eba8f37df995", + "sha256:9d151bfb0e751e2c987f931c57792871c8d7ff292bcdfcaa7233012c367940ee", + "sha256:b932b127da810fef57d427260dde1ad54542c136c44b227a1e367551bb1a684b", + "sha256:cfb862aa37f4dd5be0730731fdb8185ac935aba8b51bf3bd035658111c9ee1c9", + "sha256:de7ecb4b120e98b91e8a2a21f186571266a8d1faa31d92421e979c7ca67d8e5c", + "sha256:df7e1933a0b83920769611c5d6b9a1bf301e3fa6a544641c6678c67621fe9843" ], "index": "pypi", - "version": "==0.24.2" + "version": "==0.25.0" }, "pandas-ods-reader": { "hashes": [ - "sha256:0f7d510639c8957a06aa1227b9f84d1be47a437dfd306464ce803b91cf5eeec4", - "sha256:d85ef58fc3aeac1616028d22954b6ef2e8983ab9bae015e1e90ce3979d138553" + "sha256:d2d6e4f9cd2850da32808bbc68d433a337911058387992026d3987ead1f4a7c8", + "sha256:d4d6781cc46e782e265b48681416f636e7659343dec948c6fccc4236af6fa1e6" ], "index": "pypi", - "version": "==0.0.6" + "version": "==0.0.7" }, "passivetotal": { "hashes": [ - "sha256:d745a6519ec04e3a354682978ebf07778bf7602beac30307cbad075ff1a4418d" + "sha256:2944974d380a41f19f8fbb3d7cbfc8285479eb81092940b57bf0346d66706a05", + "sha256:a0cbea84b0bd6e9f3694ddeb447472b3d6f09e28940a7a0388456b8cf6a8e478", + "sha256:e35bf2cbccb385795a67d66f180d14ce9136cf1611b1c3da8a1055a1aced6264" ], "index": "pypi", - "version": "==1.0.30" + "version": "==1.0.31" }, "pdftotext": { "hashes": [ @@ -415,54 +416,54 @@ }, "pillow": { "hashes": [ - "sha256:15c056bfa284c30a7f265a41ac4cbbc93bdbfc0dfe0613b9cb8a8581b51a9e55", - "sha256:1a4e06ba4f74494ea0c58c24de2bb752818e9d504474ec95b0aa94f6b0a7e479", - "sha256:1c3c707c76be43c9e99cb7e3d5f1bee1c8e5be8b8a2a5eeee665efbf8ddde91a", - "sha256:1fd0b290203e3b0882d9605d807b03c0f47e3440f97824586c173eca0aadd99d", - "sha256:24114e4a6e1870c5a24b1da8f60d0ba77a0b4027907860188ea82bd3508c80eb", - "sha256:258d886a49b6b058cd7abb0ab4b2b85ce78669a857398e83e8b8e28b317b5abb", - "sha256:33c79b6dd6bc7f65079ab9ca5bebffb5f5d1141c689c9c6a7855776d1b09b7e8", - "sha256:367385fc797b2c31564c427430c7a8630db1a00bd040555dfc1d5c52e39fcd72", - "sha256:3c1884ff078fb8bf5f63d7d86921838b82ed4a7d0c027add773c2f38b3168754", - "sha256:44e5240e8f4f8861d748f2a58b3f04daadab5e22bfec896bf5434745f788f33f", - "sha256:46aa988e15f3ea72dddd81afe3839437b755fffddb5e173886f11460be909dce", - "sha256:74d90d499c9c736d52dd6d9b7221af5665b9c04f1767e35f5dd8694324bd4601", - "sha256:809c0a2ce9032cbcd7b5313f71af4bdc5c8c771cb86eb7559afd954cab82ebb5", - "sha256:85d1ef2cdafd5507c4221d201aaf62fc9276f8b0f71bd3933363e62a33abc734", - "sha256:8c3889c7681af77ecfa4431cd42a2885d093ecb811e81fbe5e203abc07e0995b", - "sha256:9218d81b9fca98d2c47d35d688a0cea0c42fd473159dfd5612dcb0483c63e40b", - "sha256:9aa4f3827992288edd37c9df345783a69ef58bd20cc02e64b36e44bcd157bbf1", - "sha256:9d80f44137a70b6f84c750d11019a3419f409c944526a95219bea0ac31f4dd91", - "sha256:b7ebd36128a2fe93991293f997e44be9286503c7530ace6a55b938b20be288d8", - "sha256:c4c78e2c71c257c136cdd43869fd3d5e34fc2162dc22e4a5406b0ebe86958239", - "sha256:c6a842537f887be1fe115d8abb5daa9bc8cc124e455ff995830cc785624a97af", - "sha256:cf0a2e040fdf5a6d95f4c286c6ef1df6b36c218b528c8a9158ec2452a804b9b8", - "sha256:cfd28aad6fc61f7a5d4ee556a997dc6e5555d9381d1390c00ecaf984d57e4232", - "sha256:dca5660e25932771460d4688ccbb515677caaf8595f3f3240ec16c117deff89a", - "sha256:de7aedc85918c2f887886442e50f52c1b93545606317956d65f342bd81cb4fc3", - "sha256:e6c0bbf8e277b74196e3140c35f9a1ae3eafd818f7f2d3a15819c49135d6c062" + "sha256:0804f77cb1e9b6dbd37601cee11283bba39a8d44b9ddb053400c58e0c0d7d9de", + "sha256:0ab7c5b5d04691bcbd570658667dd1e21ca311c62dcfd315ad2255b1cd37f64f", + "sha256:0b3e6cf3ea1f8cecd625f1420b931c83ce74f00c29a0ff1ce4385f99900ac7c4", + "sha256:365c06a45712cd723ec16fa4ceb32ce46ad201eb7bbf6d3c16b063c72b61a3ed", + "sha256:38301fbc0af865baa4752ddae1bb3cbb24b3d8f221bf2850aad96b243306fa03", + "sha256:3aef1af1a91798536bbab35d70d35750bd2884f0832c88aeb2499aa2d1ed4992", + "sha256:3fe0ab49537d9330c9bba7f16a5f8b02da615b5c809cdf7124f356a0f182eccd", + "sha256:45a619d5c1915957449264c81c008934452e3fd3604e36809212300b2a4dab68", + "sha256:49f90f147883a0c3778fd29d3eb169d56416f25758d0f66775db9184debc8010", + "sha256:571b5a758baf1cb6a04233fb23d6cf1ca60b31f9f641b1700bfaab1194020555", + "sha256:5ac381e8b1259925287ccc5a87d9cf6322a2dc88ae28a97fe3e196385288413f", + "sha256:6153db744a743c0c8c91b8e3b9d40e0b13a5d31dbf8a12748c6d9bfd3ddc01ad", + "sha256:6fd63afd14a16f5d6b408f623cc2142917a1f92855f0df997e09a49f0341be8a", + "sha256:70acbcaba2a638923c2d337e0edea210505708d7859b87c2bd81e8f9902ae826", + "sha256:70b1594d56ed32d56ed21a7fbb2a5c6fd7446cdb7b21e749c9791eac3a64d9e4", + "sha256:76638865c83b1bb33bcac2a61ce4d13c17dba2204969dedb9ab60ef62bede686", + "sha256:7b2ec162c87fc496aa568258ac88631a2ce0acfe681a9af40842fc55deaedc99", + "sha256:7cee2cef07c8d76894ebefc54e4bb707dfc7f258ad155bd61d87f6cd487a70ff", + "sha256:7d16d4498f8b374fc625c4037742fbdd7f9ac383fd50b06f4df00c81ef60e829", + "sha256:b50bc1780681b127e28f0075dfb81d6135c3a293e0c1d0211133c75e2179b6c0", + "sha256:bd0582f831ad5bcad6ca001deba4568573a4675437db17c4031939156ff339fa", + "sha256:cfd40d8a4b59f7567620410f966bb1f32dc555b2b19f82a91b147fac296f645c", + "sha256:e3ae410089de680e8f84c68b755b42bc42c0ceb8c03dbea88a5099747091d38e", + "sha256:e9046e559c299b395b39ac7dbf16005308821c2f24a63cae2ab173bd6aa11616", + "sha256:ef6be704ae2bc8ad0ebc5cb850ee9139493b0fc4e81abcc240fb392a63ebc808", + "sha256:f8dc19d92896558f9c4317ee365729ead9d7bbcf2052a9a19a3ef17abbb8ac5b" ], "index": "pypi", - "version": "==6.0.0" + "version": "==6.1.0" }, "psutil": { "hashes": [ - "sha256:206eb909aa8878101d0eca07f4b31889c748f34ed6820a12eb3168c7aa17478e", - "sha256:649f7ffc02114dced8fbd08afcd021af75f5f5b2311bc0e69e53e8f100fe296f", - "sha256:6ebf2b9c996bb8c7198b385bade468ac8068ad8b78c54a58ff288cd5f61992c7", - "sha256:753c5988edc07da00dafd6d3d279d41f98c62cd4d3a548c4d05741a023b0c2e7", - "sha256:76fb0956d6d50e68e3f22e7cc983acf4e243dc0fcc32fd693d398cb21c928802", - "sha256:828e1c3ca6756c54ac00f1427fdac8b12e21b8a068c3bb9b631a1734cada25ed", - "sha256:a4c62319ec6bf2b3570487dd72d471307ae5495ce3802c1be81b8a22e438b4bc", - "sha256:acba1df9da3983ec3c9c963adaaf530fcb4be0cd400a8294f1ecc2db56499ddd", - "sha256:ef342cb7d9b60e6100364f50c57fa3a77d02ff8665d5b956746ac01901247ac4" + "sha256:028a1ec3c6197eadd11e7b46e8cc2f0720dc18ac6d7aabdb8e8c0d6c9704f000", + "sha256:503e4b20fa9d3342bcf58191bbc20a4a5ef79ca7df8972e6197cc14c5513e73d", + "sha256:863a85c1c0a5103a12c05a35e59d336e1d665747e531256e061213e2e90f63f3", + "sha256:954f782608bfef9ae9f78e660e065bd8ffcfaea780f9f2c8a133bb7cb9e826d7", + "sha256:b6e08f965a305cd84c2d07409bc16fbef4417d67b70c53b299116c5b895e3f45", + "sha256:bc96d437dfbb8865fc8828cf363450001cb04056bbdcdd6fc152c436c8a74c61", + "sha256:cf49178021075d47c61c03c0229ac0c60d5e2830f8cab19e2d88e579b18cdb76", + "sha256:d5350cb66690915d60f8b233180f1e49938756fb2d501c93c44f8fb5b970cc63", + "sha256:eba238cf1989dfff7d483c029acb0ac4fcbfc15de295d682901f0e2497e6781a" ], - "version": "==5.6.2" + "version": "==5.6.3" }, "pybgpranking": { "editable": true, "git": "https://github.com/D4-project/BGP-Ranking.git/", - "ref": "429cea9c0787876820984a2df4e982449a84c10e", + "ref": "331bdf499c4dc19c3404e85ce0dc1ff161d35250", "subdirectory": "client" }, "pydnstrails": { @@ -493,13 +494,13 @@ "pyipasnhistory": { "editable": true, "git": "https://github.com/D4-project/IPASN-History.git/", - "ref": "47cd0f2658ab172fce42126ff3a1dbcddfb0b5fb", + "ref": "32b3bb13967527a4a42eb56f226bf03a04da3cc8", "subdirectory": "client" }, "pymisp": { "editable": true, "git": "https://github.com/MISP/PyMISP.git", - "ref": "583fb6592495ea358aad47a8a1ec92d43c13348a" + "ref": "b5226a959c72e5b414a3ce297d3865bbb9fd0da2" }, "pyonyphe": { "editable": true, @@ -530,16 +531,16 @@ }, "pyrsistent": { "hashes": [ - "sha256:16692ee739d42cf5e39cef8d27649a8c1fdb7aa99887098f1460057c5eb75c3a" + "sha256:50cffebc87ca91b9d4be2dcc2e479272bcb466b5a0487b6c271f7ddea6917e14" ], - "version": "==0.15.2" + "version": "==0.15.3" }, "pytesseract": { "hashes": [ - "sha256:11c20321595b6e2e904b594633edf1a717212b13bac7512986a2d807b8849770" + "sha256:46363b300d6890d24782852e020c06e96344529fead98f3b9b8506c82c37db6f" ], "index": "pypi", - "version": "==0.2.6" + "version": "==0.2.7" }, "python-dateutil": { "hashes": [ @@ -571,19 +572,19 @@ }, "pyyaml": { "hashes": [ - "sha256:1adecc22f88d38052fb787d959f003811ca858b799590a5eaa70e63dca50308c", - "sha256:436bc774ecf7c103814098159fbb84c2715d25980175292c648f2da143909f95", - "sha256:460a5a4248763f6f37ea225d19d5c205677d8d525f6a83357ca622ed541830c2", - "sha256:5a22a9c84653debfbf198d02fe592c176ea548cccce47553f35f466e15cf2fd4", - "sha256:7a5d3f26b89d688db27822343dfa25c599627bc92093e788956372285c6298ad", - "sha256:9372b04a02080752d9e6f990179a4ab840227c6e2ce15b95e1278456664cf2ba", - "sha256:a5dcbebee834eaddf3fa7366316b880ff4062e4bcc9787b78c7fbb4a26ff2dd1", - "sha256:aee5bab92a176e7cd034e57f46e9df9a9862a71f8f37cad167c6fc74c65f5b4e", - "sha256:c51f642898c0bacd335fc119da60baae0824f2cde95b0330b56c0553439f0673", - "sha256:c68ea4d3ba1705da1e0d85da6684ac657912679a649e8868bd850d2c299cce13", - "sha256:e23d0cc5299223dcc37885dae624f382297717e459ea24053709675a976a3e19" + "sha256:57acc1d8533cbe51f6662a55434f0dbecfa2b9eaf115bede8f6fd00115a0c0d3", + "sha256:588c94b3d16b76cfed8e0be54932e5729cc185caffaa5a451e7ad2f7ed8b4043", + "sha256:68c8dd247f29f9a0d09375c9c6b8fdc64b60810ebf07ba4cdd64ceee3a58c7b7", + "sha256:70d9818f1c9cd5c48bb87804f2efc8692f1023dac7f1a1a5c61d454043c1d265", + "sha256:86a93cccd50f8c125286e637328ff4eef108400dd7089b46a7be3445eecfa391", + "sha256:a0f329125a926876f647c9fa0ef32801587a12328b4a3c741270464e3e4fa778", + "sha256:a3c252ab0fa1bb0d5a3f6449a4826732f3eb6c0270925548cac342bc9b22c225", + "sha256:b4bb4d3f5e232425e25dda21c070ce05168a786ac9eda43768ab7f3ac2770955", + "sha256:cd0618c5ba5bda5f4039b9398bb7fb6a317bb8298218c3de25c47c4740e4b95e", + "sha256:ceacb9e5f8474dcf45b940578591c7f3d960e82f926c707788a570b51ba59190", + "sha256:fe6a88094b64132c4bb3b631412e90032e8cfe9745a58370462240b8cb7553cd" ], - "version": "==5.1" + "version": "==5.1.1" }, "pyzbar": { "hashes": [ @@ -610,37 +611,37 @@ }, "reportlab": { "hashes": [ - "sha256:04b9bf35127974f734bddddf48860732361e31c1220c0ebe4f683f19d5cfc3b8", - "sha256:073da867efdf9e0d6cba2a566f5929ef0bb9fb757b53a7132b91db9869441859", - "sha256:08e6e63a4502d3a00062ba9ff9669f95577fbdb1a5f8c6cdb1230c5ee295273a", - "sha256:0960567b9d937a288efa04753536dce1dbb032a1e1f622fd92efbe85b8cccf6e", - "sha256:1870e321c5d7772fd6e5538a89562ed8b40687ed0aec254197dc73e9d700e62f", - "sha256:1eac902958a7f66c30e1115fa1a80bf6a7aa57680427cfcb930e13c746142150", - "sha256:1f6cdcdaf6ab78ab3efd21b23c27e4487a5c0816202c3578b277f441f984a51f", - "sha256:281443252a335489ce4b8b150afccdc01c74daf97e962fd99a8c2d59c8b333d3", - "sha256:2ae66e61b03944c5ed1f3c96bbc51160cce4aa28cbe96f205b464017cdfc851c", - "sha256:34d348575686390676757876fef50f6e32e3a59ff7d549e022b5f3b8a9f7e564", - "sha256:508224a11ec9ef203ae2fd2177e903d36d3b840eeb8ac70747f53eeb373db439", - "sha256:5c497c9597a346d27007507cddc2a792f8ca5017268738fd35c374c224d81988", - "sha256:6e0d9efe78526ddf5ad1d2357f6b2b0f5d7df354ac559358e3d056bdd12fdabf", - "sha256:817dfd400c5e694cbb6eb87bc932cd3d97cf5d79d918329b8f99085a7979bb29", - "sha256:8d6ed4357eb0146501ebdb7226c87ef98a9bcbc6d54401ec676fa905b6355e00", - "sha256:8e681324ce457cc3d5c0949c92d590ac4401347b5df55f6fde207b42316d42d2", - "sha256:926981544d37554b44c6f067c3f94981831f9ef3f2665fa5f4114b23a140f596", - "sha256:92a0bf5cc2d9418115bff46032964d25bb21c0ac8bcdf6bee5769ca810a54a5a", - "sha256:9a3e7495e223fc4a9bdcd356972c230d32bf8c7a57442ca5b8c2ff6b19e6007b", - "sha256:a31f424020176e96a0ff0229f7f251d865c5409ddf074f695b97ba604f173b48", - "sha256:aa0c35b22929c19ecd48d5c1734e420812f269f463d1ef138e0adb28069c3150", - "sha256:b36b555cdbdd51f9f00a7606966ec6d4d30d74c61d1523a1ac56bbeb83a15ed3", - "sha256:cd3d9765b8f446c25d75a4456d8781c4781de0f10f860dff5cb69bbe526e8f53", - "sha256:d3daa4f19d1dc2fc1fc2591e1354edd95439b9e9953ca8b374d41524d434b315", - "sha256:d8f1878bc1fc91c63431e9b0f1940ff18b70c059f6d38f2be1e34ce9ffcc28ea", - "sha256:ddca7479d29f9dfbfc69057764239ec7753b49a3b0dcbed08f70cbef8fccfee6", - "sha256:f28f3a965d15c88c797cf33968bdaa5a04aabcf321d3f6fcf14d7e7fde8d90f3", - "sha256:fcca214bf340f59245fff792134a9ac333d21eeef19a874a69ecc926b4c992a4" + "sha256:065bca611829da371df97cec255239a2972119afbab57528022df8b41881a3f6", + "sha256:329843edd93293a96b99b2e9c226066a9ed27f0f881b4933536577e1dab898cf", + "sha256:393140710488b7ffda2762a08f63671dcccdbccfed0e4c8e8ec77e5a355080a1", + "sha256:3c778843f50981a1569539120f0cfa2be0ca7a80e4c61bdfc88a74c323b90b00", + "sha256:44ab0741f40899936e7cc85b0a19614a483da4b476102ac58d1ac20ef6da9fc3", + "sha256:4582272135bd2f355a616b4ac08310947d88b0d3e4f474be16175d89fa200c0d", + "sha256:47612270365e21581178ebbb91edabf9b3c6b4519baf2052d3f4cbe302e3ea76", + "sha256:4f8c5e65fcfa111be309228efca92ba17f329d3dbf3bbe055094fe907ab5d4c8", + "sha256:4ff4942cb1ca1f70a890fd35c7e1d0657d08dbdf6bdb5bc2c0dd3e30a6301cf7", + "sha256:5b109b347ae391963ef846e41c4c65c2bc99e81f1d4eeff687635b73ee952bf5", + "sha256:5cbd56e8dea652f73f728578cb3dbc57bd100f308012fe90596085520d2cb25a", + "sha256:5dddc51b5848a2d0a6fe47e96496220a305e7d796d4a6973cc984ab1d8160ff7", + "sha256:6c81ee26753fa09062d8404f6340eefb02849608b619e3843e0d17a7cda8798f", + "sha256:706ffb184c4cdeabcaef3b9eaba86cbf7684467c32d308ed908917fc679f86c8", + "sha256:794499adc5ad419e064523f13b0782ee2860180e79c8cd02379c4c957e1f0abb", + "sha256:8b7fcc98b0aed3e3e4f134f4d5a498bb9c068fdce6c6b2a9f103d3a339efd8d1", + "sha256:8bc0fe11be68207866902ee96eec6645d574d82fd6abd93c8bcdcd57ac1b4040", + "sha256:92f01e16fe65e51ffa2fe0e37da697c8b8f5d892605c05394c883a866a11efc1", + "sha256:a162484b22c52ab701b74f8c35b2a14f9ecf9694f2ab149fb38f377069743e69", + "sha256:a30b42d6c5ffe1ce7c677328a47386f861c3bb9057bf4de5eb0f97fe17e9b3ba", + "sha256:a7a63d35c59af1d134ec43bab75070af86e59c412289198de3788765627a611c", + "sha256:aee6aa362cbaf9abc406944064a887a69f6f5606fa54abaecf98a78459d1d954", + "sha256:ba537b091614f3839716fb7b418e157216e213a0eab3fe7db2dfbf198fb61224", + "sha256:be8f70ec622b98ef830af5591ab4c0b062a67507a19ca43327da5ff350435b43", + "sha256:c380bcb032736d45bd9a90f4208547a679b7fe2327fc1187a73a2d9b58988f1d", + "sha256:cd2fdcd1e31113878d5c5c9ae17a34368a13e1c9e12d586b66b77ff806371e23", + "sha256:f59d772b504035b1468544a11269ee27648ddb2fae1efddd45ce050da2527813", + "sha256:ff1570bf8ad010c408f72822248ad2276185d473ab9a64c70ad2ec4427dda052" ], "index": "pypi", - "version": "==3.5.21" + "version": "==3.5.23" }, "requests": { "hashes": [ @@ -659,17 +660,17 @@ }, "shodan": { "hashes": [ - "sha256:4aa8ea11448159147dbdf65b2aa3b10d47c05decd94992fdd016efdc7781e91b" + "sha256:13953527d0a1a86d2346631143066533a6f804551a77e40284d1dc53ce28bd30" ], "index": "pypi", - "version": "==1.13.0" + "version": "==1.14.0" }, "sigmatools": { "hashes": [ - "sha256:ae980b6d6fd466294911efa493934d24e3c5df406da4a190b9fff0943a81cc5f" + "sha256:f28838a26f8a0be066da38dd65b70e3241d109037029bb69069079e2fa3dfdbc" ], "index": "pypi", - "version": "==0.10" + "version": "==0.11" }, "six": { "hashes": [ @@ -680,10 +681,10 @@ }, "soupsieve": { "hashes": [ - "sha256:6898e82ecb03772a0d82bd0d0a10c0d6dcc342f77e0701d0ec4a8271be465ece", - "sha256:b20eff5e564529711544066d7dc0f7661df41232ae263619dede5059799cdfca" + "sha256:72b5f1aea9101cf720a36bb2327ede866fd6f1a07b1e87c92a1cc18113cbc946", + "sha256:e4e9c053d59795e440163733a7fec6c5972210e1790c507e4c7b051d6c5259de" ], - "version": "==1.9.1" + "version": "==1.9.2" }, "sparqlwrapper": { "hashes": [ @@ -709,15 +710,15 @@ }, "tornado": { "hashes": [ - "sha256:1174dcb84d08887b55defb2cda1986faeeea715fff189ef3dc44cce99f5fca6b", - "sha256:2613fab506bd2aedb3722c8c64c17f8f74f4070afed6eea17f20b2115e445aec", - "sha256:44b82bc1146a24e5b9853d04c142576b4e8fa7a92f2e30bc364a85d1f75c4de2", - "sha256:457fcbee4df737d2defc181b9073758d73f54a6cfc1f280533ff48831b39f4a8", - "sha256:49603e1a6e24104961497ad0c07c799aec1caac7400a6762b687e74c8206677d", - "sha256:8c2f40b99a8153893793559919a355d7b74649a11e59f411b0b0a1793e160bc0", - "sha256:e1d897889c3b5a829426b7d52828fb37b28bc181cd598624e65c8be40ee3f7fa" + "sha256:349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c", + "sha256:398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60", + "sha256:4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281", + "sha256:559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5", + "sha256:abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7", + "sha256:c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9", + "sha256:c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5" ], - "version": "==6.0.2" + "version": "==6.0.3" }, "url-normalize": { "hashes": [ @@ -757,12 +758,17 @@ }, "wand": { "hashes": [ - "sha256:63ab24dee0264a44f5f045d4ecc0d392bc1cc195e5a2f80ce537b2c205c3033b", - "sha256:a2c318993791fab4fcfd460045415176f81d42f8c6fd8a88fb8d74d2f0f34b97", - "sha256:f68f32f2e4eca663a361d36148f06372de560442dcf8c785a53a64ee282572c9" + "sha256:1d3808e5d7a722096866b1eaa1743f29eb663289e140c5306d6291e1d581fed5", + "sha256:c97029751f595d96ae0042aec0e26ff114e403e060ae2481124abbcca0c65ce2" ], "index": "pypi", - "version": "==0.5.3" + "version": "==0.5.5" + }, + "wrapt": { + "hashes": [ + "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" + ], + "version": "==1.11.2" }, "xlrd": { "hashes": [ @@ -830,10 +836,10 @@ }, "certifi": { "hashes": [ - "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", - "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae" + "sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", + "sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695" ], - "version": "==2019.3.9" + "version": "==2019.6.16" }, "chardet": { "hashes": [ @@ -895,11 +901,11 @@ }, "flake8": { "hashes": [ - "sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661", - "sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8" + "sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", + "sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696" ], "index": "pypi", - "version": "==3.7.7" + "version": "==3.7.8" }, "idna": { "hashes": [ @@ -908,6 +914,13 @@ ], "version": "==2.8" }, + "importlib-metadata": { + "hashes": [ + "sha256:6dfd58dfe281e8d240937776065dd3624ad5469c835248219bd16cf2e12dbeb7", + "sha256:cb6ee23b46173539939964df59d3d72c3e0c1b5d54b84f1d8a7e912fe43612db" + ], + "version": "==0.18" + }, "mccabe": { "hashes": [ "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", @@ -917,11 +930,10 @@ }, "more-itertools": { "hashes": [ - "sha256:2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7", - "sha256:c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a" + "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", + "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" ], - "markers": "python_version > '2.7'", - "version": "==7.0.0" + "version": "==7.2.0" }, "nose": { "hashes": [ @@ -932,12 +944,19 @@ "index": "pypi", "version": "==1.3.7" }, + "packaging": { + "hashes": [ + "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af", + "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3" + ], + "version": "==19.0" + }, "pluggy": { "hashes": [ - "sha256:25a1bc1d148c9a640211872b4ff859878d422bccb59c9965e04eed468a0aa180", - "sha256:964cedd2b27c492fbf0b7f58b3284a09cf7f99b0f715941fb24a439b3af1bd1a" + "sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc", + "sha256:b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c" ], - "version": "==0.11.0" + "version": "==0.12.0" }, "py": { "hashes": [ @@ -960,13 +979,20 @@ ], "version": "==2.1.1" }, + "pyparsing": { + "hashes": [ + "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a", + "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03" + ], + "version": "==2.4.0" + }, "pytest": { "hashes": [ - "sha256:1a8aa4fa958f8f451ac5441f3ac130d9fc86ea38780dd2715e6d5c5882700b24", - "sha256:b8bf138592384bd4e87338cb0f256bf5f615398a649d4bd83915f0e4047a5ca6" + "sha256:6ef6d06de77ce2961156013e9dff62f1b2688aa04d0dc244299fe7d67e09370d", + "sha256:a736fed91c12681a7b34617c8fcefe39ea04599ca72c608751c31d89579a3f77" ], "index": "pypi", - "version": "==4.5.0" + "version": "==5.0.1" }, "requests": { "hashes": [ @@ -996,6 +1022,13 @@ "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" ], "version": "==0.1.7" + }, + "zipp": { + "hashes": [ + "sha256:4970c3758f4e89a7857a973b1e2a5d75bcdc47794442f2e2dd4fe8e0466e809a", + "sha256:8a5712cfd3bb4248015eb3b0b3c54a5f6ee3f2425963ef2a0125b8bc40aafaec" + ], + "version": "==0.5.2" } } } diff --git a/tests/test_expansions.py b/tests/test_expansions.py index d581a31..d0ae018 100644 --- a/tests/test_expansions.py +++ b/tests/test_expansions.py @@ -37,7 +37,7 @@ class TestExpansions(unittest.TestCase): def test_haveibeenpwned(self): query = {"module": "hibp", "email-src": "info@circl.lu"} response = self.misp_modules_post(query) - self.assertEqual(self.get_values(response), 'OK (Not Found)') + self.assertEqual(self.get_values(response), 'OK (Not Found)', response) def test_greynoise(self): query = {"module": "greynoise", "ip-dst": "1.1.1.1"} From 40c70c1a5370680b0dbfa14858d5f3ba4d1d67fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Wed, 24 Jul 2019 09:35:55 +0200 Subject: [PATCH 194/207] chg: Add print to figure out what's going on on travis. --- tests/test_expansions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_expansions.py b/tests/test_expansions.py index d0ae018..1686a8f 100644 --- a/tests/test_expansions.py +++ b/tests/test_expansions.py @@ -4,6 +4,7 @@ import unittest import requests from urllib.parse import urljoin +import json class TestExpansions(unittest.TestCase): @@ -17,7 +18,9 @@ class TestExpansions(unittest.TestCase): return requests.post(urljoin(self.url, "query"), json=query) def get_values(self, response): - return response.json()['results'][0]['values'] + data = response.json() + print(json.dumps(data, indent=2)) + return data['results'][0]['values'] def test_cve(self): query = {"module": "cve", "vulnerability": "CVE-2010-3333"} From 80ce0a58b5959ab781cfb9001efd6a7c34f2fe5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Wed, 24 Jul 2019 09:49:05 +0200 Subject: [PATCH 195/207] fix: Skip tests on haveibeenpwned.com if 403. Make pep8 happy. --- misp_modules/modules/expansion/virustotal.py | 4 ++-- tests/test_expansions.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/misp_modules/modules/expansion/virustotal.py b/misp_modules/modules/expansion/virustotal.py index d962691..9660b5f 100644 --- a/misp_modules/modules/expansion/virustotal.py +++ b/misp_modules/modules/expansion/virustotal.py @@ -40,7 +40,7 @@ class VirusTotalParser(object): return {'results': results} ################################################################################ - #### Main parsing functions #### + #### Main parsing functions #### # noqa ################################################################################ def parse_domain(self, domain, recurse=False): @@ -123,7 +123,7 @@ class VirusTotalParser(object): return status_code ################################################################################ - #### Additional parsing functions #### + #### Additional parsing functions #### # noqa ################################################################################ def parse_related_urls(self, query_result, recurse, uuid=None): diff --git a/tests/test_expansions.py b/tests/test_expansions.py index 1686a8f..84ca713 100644 --- a/tests/test_expansions.py +++ b/tests/test_expansions.py @@ -20,6 +20,8 @@ class TestExpansions(unittest.TestCase): def get_values(self, response): data = response.json() print(json.dumps(data, indent=2)) + if not isinstance(data, dict): + return data return data['results'][0]['values'] def test_cve(self): @@ -40,6 +42,8 @@ class TestExpansions(unittest.TestCase): def test_haveibeenpwned(self): query = {"module": "hibp", "email-src": "info@circl.lu"} response = self.misp_modules_post(query) + if response == "haveibeenpwned.com API not accessible (HTTP 403)": + self.skipTest(f"haveibeenpwned blocks travis IPs: {response}") self.assertEqual(self.get_values(response), 'OK (Not Found)', response) def test_greynoise(self): From fee889f71c1161b6eb870e364493f480046e84d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Wed, 24 Jul 2019 09:57:52 +0200 Subject: [PATCH 196/207] fix: Wrong change in last commit. --- tests/test_expansions.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_expansions.py b/tests/test_expansions.py index 84ca713..493cb4d 100644 --- a/tests/test_expansions.py +++ b/tests/test_expansions.py @@ -19,8 +19,8 @@ class TestExpansions(unittest.TestCase): def get_values(self, response): data = response.json() - print(json.dumps(data, indent=2)) if not isinstance(data, dict): + print(json.dumps(data, indent=2)) return data return data['results'][0]['values'] @@ -42,9 +42,10 @@ class TestExpansions(unittest.TestCase): def test_haveibeenpwned(self): query = {"module": "hibp", "email-src": "info@circl.lu"} response = self.misp_modules_post(query) - if response == "haveibeenpwned.com API not accessible (HTTP 403)": + to_check = self.get_values(response) + if to_check == "haveibeenpwned.com API not accessible (HTTP 403)": self.skipTest(f"haveibeenpwned blocks travis IPs: {response}") - self.assertEqual(self.get_values(response), 'OK (Not Found)', response) + self.assertEqual(to_check, 'OK (Not Found)', response) def test_greynoise(self): query = {"module": "greynoise", "ip-dst": "1.1.1.1"} From 4ee0cbe4c57097ca73b5d4d265759758a400e68b Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Wed, 24 Jul 2019 11:10:25 +0200 Subject: [PATCH 197/207] add: Added virustotal_public to the list of available modules --- misp_modules/modules/expansion/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/__init__.py b/misp_modules/modules/expansion/__init__.py index 960db23..ef31ad9 100644 --- a/misp_modules/modules/expansion/__init__.py +++ b/misp_modules/modules/expansion/__init__.py @@ -13,4 +13,5 @@ __all__ = ['cuckoo_submit', 'vmray_submit', 'bgpranking', 'circl_passivedns', 'c 'sigma_queries', 'dbl_spamhaus', 'vulners', 'yara_query', 'macaddress_io', 'intel471', 'backscatter_io', 'btc_scam_check', 'hibp', 'greynoise', 'macvendors', 'qrcode', 'ocr-enrich', 'pdf-enrich', 'docx-enrich', 'xlsx-enrich', 'pptx-enrich', - 'ods-enrich', 'odt-enrich', 'joesandbox_submit', 'joesandbox_query', 'urlhaus'] + 'ods-enrich', 'odt-enrich', 'joesandbox_submit', 'joesandbox_query', 'urlhaus', + 'virustotal_public'] From fc8a573ba7334d0829b6e617a0bf1b815f3e1602 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Wed, 24 Jul 2019 11:14:12 +0200 Subject: [PATCH 198/207] fix: Changed the way references added at the end are saved - Some references are saved until they are added at the end, to make it easier when needed - Here we changed the way they are saved, from a dictionary with some keys to identify each part to the actual dictionary with the keys the function add_reference needs, so we can directly use this dictionary as is when the references are added to the different objects --- misp_modules/lib/joe_parser.py | 36 ++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/misp_modules/lib/joe_parser.py b/misp_modules/lib/joe_parser.py index d957d69..d6f49cf 100644 --- a/misp_modules/lib/joe_parser.py +++ b/misp_modules/lib/joe_parser.py @@ -74,7 +74,7 @@ class JoeParser(): object_uuid = misp_object.uuid if object_uuid in self.references: for reference in self.references[object_uuid]: - misp_object.add_reference(reference['idref'], reference['relationship']) + misp_object.add_reference(**reference) def handle_attributes(self): for attribute_type, attribute in self.attributes.items(): @@ -82,7 +82,8 @@ class JoeParser(): attribute_uuid = self.create_attribute(attribute_type, attribute_value) for reference in references: source_uuid, relationship = reference - self.references[source_uuid].append({'idref': attribute_uuid, 'relationship': relationship}) + self.references[source_uuid].append(dict(referenced_uuid=attribute_uuid, + relationship_type=relationship)) def parse_dropped_files(self): droppedinfo = self.data['droppedinfo'] @@ -99,8 +100,8 @@ class JoeParser(): file_object.add_attribute(hash_type, **{'type': hash_type, 'value': h['$']}) self.misp_event.add_object(**file_object) self.references[self.process_references[(int(droppedfile['@targetid']), droppedfile['@process'])]].append({ - 'idref': file_object.uuid, - 'relationship': 'drops' + 'referenced_uuid': file_object.uuid, + 'relationship_type': 'drops' }) def parse_mitre_attack(self): @@ -130,7 +131,8 @@ class JoeParser(): for protocol in data.keys(): network_connection_object.add_attribute('layer{}-protocol'.format(protocols[protocol]), **{'type': 'text', 'value': protocol}) self.misp_event.add_object(**network_connection_object) - self.references[self.analysisinfo_uuid].append({'idref': network_connection_object.uuid, 'relationship': 'initiates'}) + self.references[self.analysisinfo_uuid].append(dict(referenced_uuid=network_connection_object.uuid, + relationship_type='initiates')) else: for protocol, timestamps in data.items(): network_connection_object = MISPObject('network-connection') @@ -139,7 +141,8 @@ class JoeParser(): network_connection_object.add_attribute('first-packet-seen', **{'type': 'datetime', 'value': min(timestamps)}) network_connection_object.add_attribute('layer{}-protocol'.format(protocols[protocol]), **{'type': 'text', 'value': protocol}) self.misp_event.add_object(**network_connection_object) - self.references[self.analysisinfo_uuid].append({'idref': network_connection_object.uuid, 'relationship': 'initiates'}) + self.references[self.analysisinfo_uuid].append(dict(referenced_uuid=network_connection_object.uuid, + relationship_type='initiates')) def parse_screenshot(self): screenshotdata = self.data['behavior']['screenshotdata']['interesting']['$'] @@ -162,7 +165,8 @@ class JoeParser(): self.misp_event.add_object(**process_object) for field, to_call in process_activities.items(): to_call(process_object.uuid, process[field]) - self.references[self.analysisinfo_uuid].append({'idref': process_object.uuid, 'relationship': 'calls'}) + self.references[self.analysisinfo_uuid].append(dict(referenced_uuid=process_object.uuid, + relationship_type='calls')) self.process_references[(general['targetid'], general['path'])] = process_object.uuid def parse_fileactivities(self, process_uuid, fileactivities): @@ -240,7 +244,8 @@ class JoeParser(): self.misp_event.add_object(**pe_object) for section in peinfo['sections']['section']: section_object = self.parse_pe_section(section) - self.references[pe_object.uuid].append({'idref': section_object.uuid, 'relationship': 'included-in'}) + self.references[pe_object.uuid].append(dict(referenced_uuid=section_object.uuid, + relationship_type='included-in')) self.misp_event.add_object(**section_object) def parse_network_interactions(self): @@ -254,13 +259,13 @@ class JoeParser(): domain_object.add_attribute(object_relation, **{'type': attribute_type, 'value': domain[key]}) self.misp_event.add_object(**domain_object) - reference = {'idref': domain_object.uuid, 'relationship': 'contacts'} + reference = dict(referenced_uuid=domain_object.uuid, relationship_type='contacts') self.add_process_reference(domain['@targetid'], domain['@currentpath'], reference) else: attribute = MISPAttribute() attribute.from_dict(**{'type': 'domain', 'value': domain['@name']}) self.misp_event.add_attribute(**attribute) - reference = {'idref': attribute.uuid, 'relationship': 'contacts'} + reference = dict(referenced_uuid=attribute.uuid, relationship_type='contacts') self.add_process_reference(domain['@targetid'], domain['@currentpath'], reference) ipinfo = self.data['ipinfo'] if ipinfo: @@ -268,7 +273,7 @@ class JoeParser(): attribute = MISPAttribute() attribute.from_dict(**{'type': 'ip-dst', 'value': ip['@ip']}) self.misp_event.add_attribute(**attribute) - reference = {'idref': attribute.uuid, 'relationship': 'contacts'} + reference = dict(referenced_uuid=attribute.uuid, relationship_type='contacts') self.add_process_reference(ip['@targetid'], ip['@currentpath'], reference) urlinfo = self.data['urlinfo'] if urlinfo: @@ -279,8 +284,8 @@ class JoeParser(): attribute_dict = {'type': 'url', 'value': url['@name']} if target_id != -1 and current_path != 'unknown': self.references[self.process_references[(target_id, current_path)]].append({ - 'idref': attribute.uuid, - 'relationship': 'contacts' + 'referenced_uuid': attribute.uuid, + 'relationship_type': 'contacts' }) else: attribute_dict['comment'] = 'From Memory - Enriched via the joe_import module' @@ -298,7 +303,7 @@ class JoeParser(): if registryactivities['keyCreated']: for call in registryactivities['keyCreated']['call']: self.attributes['regkey'][call['path']].add((process_uuid, 'creates')) - for feature, relationship_type in registry_references_mapping.items(): + for feature, relationship in registry_references_mapping.items(): if registryactivities[feature]: for call in registryactivities[feature]['call']: registry_key = MISPObject('registry-key') @@ -307,7 +312,8 @@ class JoeParser(): registry_key.add_attribute(object_relation, **{'type': attribute_type, 'value': call[field]}) registry_key.add_attribute('data-type', **{'type': 'text', 'value': 'REG_{}'.format(call['type'].upper())}) self.misp_event.add_object(**registry_key) - self.references[process_uuid].append({'idref': registry_key.uuid, 'relationship': relationship_type}) + self.references[process_uuid].append(dict(referenced_uuid=registry_key.uuid, + relationship_type=relationship)) def add_process_reference(self, target, currentpath, reference): try: From 5602cf1759aff7b01b535808332a1986ee42997f Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Wed, 24 Jul 2019 11:59:11 +0200 Subject: [PATCH 199/207] add: Parsing apk samples and their permissions --- misp_modules/lib/joe_parser.py | 53 ++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/misp_modules/lib/joe_parser.py b/misp_modules/lib/joe_parser.py index d6f49cf..431640b 100644 --- a/misp_modules/lib/joe_parser.py +++ b/misp_modules/lib/joe_parser.py @@ -5,6 +5,7 @@ from pymisp import MISPAttribute, MISPEvent, MISPObject import json +arch_type_mapping = {'ANDROID': 'parse_apk', 'WINDOWS': 'parse_pe'} domain_object_mapping = {'@ip': ('ip-dst', 'ip'), '@name': ('domain', 'domain')} dropped_file_mapping = {'@entropy': ('float', 'entropy'), '@file': ('filename', 'filename'), @@ -27,17 +28,17 @@ pe_object_mapping = {'CompanyName': 'company-name', 'FileDescription': 'file-des 'LegalCopyright': 'legal-copyright', 'OriginalFilename': 'original-filename', 'ProductName': 'product-filename', 'ProductVersion': 'product-version', 'Translation': 'lang-id'} +pe_section_object_mapping = {'characteristics': ('text', 'characteristic'), + 'entropy': ('float', 'entropy'), + 'name': ('text', 'name'), 'rawaddr': ('hex', 'offset'), + 'rawsize': ('size-in-bytes', 'size-in-bytes'), + 'virtaddr': ('hex', 'virtual_address'), + 'virtsize': ('size-in-bytes', 'virtual_size')} process_object_fields = {'cmdline': 'command-line', 'name': 'name', 'parentpid': 'parent-pid', 'pid': 'pid', 'path': 'current-directory'} protocols = {'tcp': 4, 'udp': 4, 'icmp': 3, 'http': 7, 'https': 7, 'ftp': 7} -section_object_mapping = {'characteristics': ('text', 'characteristic'), - 'entropy': ('float', 'entropy'), - 'name': ('text', 'name'), 'rawaddr': ('hex', 'offset'), - 'rawsize': ('size-in-bytes', 'size-in-bytes'), - 'virtaddr': ('hex', 'virtual_address'), - 'virtsize': ('size-in-bytes', 'virtual_size')} registry_references_mapping = {'keyValueCreated': 'creates', 'keyValueModified': 'modifies'} regkey_object_mapping = {'name': ('text', 'name'), 'newdata': ('text', 'data'), 'path': ('regkey', 'key')} @@ -209,10 +210,29 @@ class JoeParser(): for field, mapping in file_object_mapping.items(): attribute_type, object_relation = mapping file_object.add_attribute(object_relation, **{'type': attribute_type, 'value': fileinfo[field]}) - if not fileinfo.get('pe'): + try: + to_call = arch_type_mapping[self.data['generalinfo']['arch']] + getattr(self, to_call)(fileinfo[to_call.split('_')[-1]], file_object) + except KeyError: self.misp_event.add_object(**file_object) - return - peinfo = fileinfo['pe'] + + def parse_apk(self, apkinfo, fileobject): + self.misp_event.add_object(**file_object) + permission_lists = defaultdict(list) + for permission in apkinfo['requiredpermissions']['permission']: + permission = permission['@name'].split('.') + permission_lists[' '.join(permission[:-1])].append(permission[-1]) + attribute_type = 'text' + for comment, permissions in permission_lists.items(): + permission_object = MISPObject('android-permission') + permission_object.add_attribute('comment', **dict(type=attribute_type, value=comment)) + for permission in permissions: + permission_object.add_attribute('permission', **dict(type=attribute_type, value=permission)) + self.misp_event.add_object(**permission_object) + self.references[file_object.uuid].append(dict(referenced_uuid=permission_object.uuid, + relationship_type='grants')) + + def parse_pe(self, peinfo, file_object): pe_object = MISPObject('pe') file_object.add_reference(pe_object.uuid, 'included-in') self.misp_event.add_object(**file_object) @@ -248,6 +268,14 @@ class JoeParser(): relationship_type='included-in')) self.misp_event.add_object(**section_object) + def parse_pe_section(self, section): + section_object = MISPObject('pe-section') + for feature, mapping in pe_section_object_mapping.items(): + if section.get(feature): + attribute_type, object_relation = mapping + section_object.add_attribute(object_relation, **{'type': attribute_type, 'value': section[feature]}) + return section_object + def parse_network_interactions(self): domaininfo = self.data['domaininfo'] if domaininfo: @@ -292,13 +320,6 @@ class JoeParser(): attribute.from_dict(**attribute_dict) self.misp_event.add_attribute(**attribute) - def parse_pe_section(self, section): - section_object = MISPObject('pe-section') - for feature, mapping in section_object_mapping.items(): - attribute_type, object_relation = mapping - section_object.add_attribute(object_relation, **{'type': attribute_type, 'value': section[feature]}) - return section_object - def parse_registryactivities(self, process_uuid, registryactivities): if registryactivities['keyCreated']: for call in registryactivities['keyCreated']['call']: From 42b95c4210a5d19b45cc368ad150438cbbd550ff Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Wed, 24 Jul 2019 12:21:58 +0200 Subject: [PATCH 200/207] fix: Fixed variable names --- misp_modules/lib/joe_parser.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/misp_modules/lib/joe_parser.py b/misp_modules/lib/joe_parser.py index 431640b..83eca3b 100644 --- a/misp_modules/lib/joe_parser.py +++ b/misp_modules/lib/joe_parser.py @@ -212,11 +212,12 @@ class JoeParser(): file_object.add_attribute(object_relation, **{'type': attribute_type, 'value': fileinfo[field]}) try: to_call = arch_type_mapping[self.data['generalinfo']['arch']] - getattr(self, to_call)(fileinfo[to_call.split('_')[-1]], file_object) + getattr(self, to_call)(fileinfo, file_object) except KeyError: self.misp_event.add_object(**file_object) - def parse_apk(self, apkinfo, fileobject): + def parse_apk(self, fileinfo, file_object): + apkinfo = fileinfo['apk'] self.misp_event.add_object(**file_object) permission_lists = defaultdict(list) for permission in apkinfo['requiredpermissions']['permission']: @@ -232,7 +233,8 @@ class JoeParser(): self.references[file_object.uuid].append(dict(referenced_uuid=permission_object.uuid, relationship_type='grants')) - def parse_pe(self, peinfo, file_object): + def parse_pe(self, fileinfo, file_object): + peinfo = fileinfo['pe'] pe_object = MISPObject('pe') file_object.add_reference(pe_object.uuid, 'included-in') self.misp_event.add_object(**file_object) From e2a0f27d75476453c85306f78f9b9109b2e42996 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Wed, 24 Jul 2019 14:58:45 +0200 Subject: [PATCH 201/207] fix: Fixed direction of the relationship between files, PEs and their sections - The file object includes a PE, and the PE includes sections, not the other way round --- misp_modules/lib/joe_parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/misp_modules/lib/joe_parser.py b/misp_modules/lib/joe_parser.py index 83eca3b..182398f 100644 --- a/misp_modules/lib/joe_parser.py +++ b/misp_modules/lib/joe_parser.py @@ -236,7 +236,7 @@ class JoeParser(): def parse_pe(self, fileinfo, file_object): peinfo = fileinfo['pe'] pe_object = MISPObject('pe') - file_object.add_reference(pe_object.uuid, 'included-in') + file_object.add_reference(pe_object.uuid, 'includes') self.misp_event.add_object(**file_object) for field, mapping in pe_object_fields.items(): attribute_type, object_relation = mapping @@ -267,7 +267,7 @@ class JoeParser(): for section in peinfo['sections']['section']: section_object = self.parse_pe_section(section) self.references[pe_object.uuid].append(dict(referenced_uuid=section_object.uuid, - relationship_type='included-in')) + relationship_type='includes')) self.misp_event.add_object(**section_object) def parse_pe_section(self, section): From 4c8fe9d8ef5a6a66765299922bb00e7afde33b6c Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Thu, 25 Jul 2019 17:43:11 +0200 Subject: [PATCH 202/207] fix: Testing if there is some screenshot data before trying to fetch it --- misp_modules/lib/joe_parser.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/misp_modules/lib/joe_parser.py b/misp_modules/lib/joe_parser.py index 182398f..4b4c4c1 100644 --- a/misp_modules/lib/joe_parser.py +++ b/misp_modules/lib/joe_parser.py @@ -146,10 +146,12 @@ class JoeParser(): relationship_type='initiates')) def parse_screenshot(self): - screenshotdata = self.data['behavior']['screenshotdata']['interesting']['$'] - attribute = {'type': 'attachment', 'value': 'screenshot.jpg', - 'data': screenshotdata, 'disable_correlation': True} - self.misp_event.add_attribute(**attribute) + screenshotdata = self.data['behavior']['screenshotdata'] + if screenshotdata: + screenshotdata = screenshotdata['interesting']['$'] + attribute = {'type': 'attachment', 'value': 'screenshot.jpg', + 'data': screenshotdata, 'disable_correlation': True} + self.misp_event.add_attribute(**attribute) def parse_system_behavior(self): system = self.data['behavior']['system'] From 41bbbeddfb1603ecebc138b1835138f0de71534a Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Thu, 25 Jul 2019 17:44:32 +0200 Subject: [PATCH 203/207] fix: Testing if file & registry activities fields exist before trying to parse it --- misp_modules/lib/joe_parser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/misp_modules/lib/joe_parser.py b/misp_modules/lib/joe_parser.py index 4b4c4c1..d980f4e 100644 --- a/misp_modules/lib/joe_parser.py +++ b/misp_modules/lib/joe_parser.py @@ -167,7 +167,8 @@ class JoeParser(): process_object.add_attribute('start-time', **{'type': 'datetime', 'value': start_time}) self.misp_event.add_object(**process_object) for field, to_call in process_activities.items(): - to_call(process_object.uuid, process[field]) + if process.get(field): + to_call(process_object.uuid, process[field]) self.references[self.analysisinfo_uuid].append(dict(referenced_uuid=process_object.uuid, relationship_type='calls')) self.process_references[(general['targetid'], general['path'])] = process_object.uuid From ddeb04bd74eae84568badb696da19827f432ce3f Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Thu, 25 Jul 2019 17:46:21 +0200 Subject: [PATCH 204/207] add: Parsing linux samples and their elf data --- misp_modules/lib/joe_parser.py | 50 ++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/misp_modules/lib/joe_parser.py b/misp_modules/lib/joe_parser.py index d980f4e..1da9abd 100644 --- a/misp_modules/lib/joe_parser.py +++ b/misp_modules/lib/joe_parser.py @@ -5,13 +5,17 @@ from pymisp import MISPAttribute, MISPEvent, MISPObject import json -arch_type_mapping = {'ANDROID': 'parse_apk', 'WINDOWS': 'parse_pe'} +arch_type_mapping = {'ANDROID': 'parse_apk', 'LINUX': 'parse_elf', 'WINDOWS': 'parse_pe'} domain_object_mapping = {'@ip': ('ip-dst', 'ip'), '@name': ('domain', 'domain')} dropped_file_mapping = {'@entropy': ('float', 'entropy'), '@file': ('filename', 'filename'), '@size': ('size-in-bytes', 'size-in-bytes'), '@type': ('mime-type', 'mimetype')} dropped_hash_mapping = {'MD5': 'md5', 'SHA': 'sha1', 'SHA-256': 'sha256', 'SHA-512': 'sha512'} +elf_object_mapping = {'epaddr': 'entrypoint-address', 'machine': 'arch', 'osabi': 'os_abi'} +elf_section_flags_mapping = {'A': 'ALLOC', 'I': 'INFO_LINK', 'M': 'MERGE', + 'S': 'STRINGS', 'T': 'TLS', 'W': 'WRITE', + 'X': 'EXECINSTR'} file_object_fields = ['filename', 'md5', 'sha1', 'sha256', 'sha512', 'ssdeep'] file_object_mapping = {'entropy': ('float', 'entropy'), 'filesize': ('size-in-bytes', 'size-in-bytes'), @@ -236,10 +240,50 @@ class JoeParser(): self.references[file_object.uuid].append(dict(referenced_uuid=permission_object.uuid, relationship_type='grants')) + def parse_elf(self, fileinfo, file_object): + elfinfo = fileinfo['elf'] + self.misp_event.add_object(**file_object) + attribute_type = 'text' + relationship = 'includes' + size = 'size-in-bytes' + for fileinfo in elfinfo['file']: + elf_object = MISPObject('elf') + self.references[file_object.uuid].append(dict(referenced_uuid=elf_object.uuid, + relationship_type=relationship)) + elf = fileinfo['main'][0]['header'][0] + if elf.get('type'): + # Haven't seen anything but EXEC yet in the files I tested + attribute_value = "EXECUTABLE" if elf['type'] == "EXEC (Executable file)" else elf['type'] + elf_object.add_attribute('type', **dict(type=attribute_type, value=attribute_value)) + for feature, relation in elf_object_mapping.items(): + if elf.get(feature): + elf_object.add_attribute(relation, **dict(type=attribute_type, value=elf[feature])) + sections_number = len(fileinfo['sections']['section']) + elf_object.add_attribute('number-sections', **{'type': 'counter', 'value': sections_number}) + self.misp_event.add_object(**elf_object) + for section in fileinfo['sections']['section']: + section_object = MISPObject('elf-section') + for feature in ('name', 'type'): + if section.get(feature): + section_object.add_attribute(feature, **dict(type=attribute_type, value=section[feature])) + if section.get('size'): + section_object.add_attribute(size, **dict(type=size, value=int(section['size'], 16))) + for flag in section['flagsdesc']: + try: + attribute_value = elf_section_flags_mapping[flag] + section_object.add_attribute('flag', **dict(type=attribute_type, value=attribute_value)) + except KeyError: + print(f'Unknown elf section flag: {flag}') + continue + self.misp_event.add_object(**section_object) + self.references[elf_object.uuid].append(dict(referenced_uuid=section_object.uuid, + relationship_type=relationship)) + def parse_pe(self, fileinfo, file_object): peinfo = fileinfo['pe'] pe_object = MISPObject('pe') - file_object.add_reference(pe_object.uuid, 'includes') + relationship = 'includes' + file_object.add_reference(pe_object.uuid, relationship) self.misp_event.add_object(**file_object) for field, mapping in pe_object_fields.items(): attribute_type, object_relation = mapping @@ -270,7 +314,7 @@ class JoeParser(): for section in peinfo['sections']['section']: section_object = self.parse_pe_section(section) self.references[pe_object.uuid].append(dict(referenced_uuid=section_object.uuid, - relationship_type='includes')) + relationship_type=relationship)) self.misp_event.add_object(**section_object) def parse_pe_section(self, section): From 3d41104d5b7c4916f92d09229959757a267de88d Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Thu, 25 Jul 2019 17:47:08 +0200 Subject: [PATCH 205/207] fix: Avoid adding file object twice if a KeyError exception comes for some unexpected reasons --- misp_modules/lib/joe_parser.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/misp_modules/lib/joe_parser.py b/misp_modules/lib/joe_parser.py index 1da9abd..f773c5d 100644 --- a/misp_modules/lib/joe_parser.py +++ b/misp_modules/lib/joe_parser.py @@ -217,10 +217,11 @@ class JoeParser(): for field, mapping in file_object_mapping.items(): attribute_type, object_relation = mapping file_object.add_attribute(object_relation, **{'type': attribute_type, 'value': fileinfo[field]}) - try: - to_call = arch_type_mapping[self.data['generalinfo']['arch']] + arch = self.data['generalinfo']['arch'] + if arch in arch_type_mapping: + to_call = arch_type_mapping[arch] getattr(self, to_call)(fileinfo, file_object) - except KeyError: + else: self.misp_event.add_object(**file_object) def parse_apk(self, fileinfo, file_object): From 3367e47490c7770a68252143003d2dc42a2f3f1d Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Thu, 25 Jul 2019 17:57:36 +0200 Subject: [PATCH 206/207] fix: Avoid issues when there is no pe field in a windows file sample analysis - For instance: doc file --- misp_modules/lib/joe_parser.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/misp_modules/lib/joe_parser.py b/misp_modules/lib/joe_parser.py index f773c5d..ccbfb7c 100644 --- a/misp_modules/lib/joe_parser.py +++ b/misp_modules/lib/joe_parser.py @@ -281,7 +281,11 @@ class JoeParser(): relationship_type=relationship)) def parse_pe(self, fileinfo, file_object): - peinfo = fileinfo['pe'] + try: + peinfo = fileinfo['pe'] + except KeyError: + self.misp_event.add_object(**file_object) + return pe_object = MISPObject('pe') relationship = 'includes' file_object.add_reference(pe_object.uuid, relationship) From 7b1c35d583a3deba6adf926fa457697becfacd79 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Tue, 30 Jul 2019 09:55:36 +0200 Subject: [PATCH 207/207] fix: Fixed cvss-score object relation name --- misp_modules/modules/expansion/cve_advanced.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/misp_modules/modules/expansion/cve_advanced.py b/misp_modules/modules/expansion/cve_advanced.py index 3a89ec9..62c49e2 100644 --- a/misp_modules/modules/expansion/cve_advanced.py +++ b/misp_modules/modules/expansion/cve_advanced.py @@ -4,7 +4,9 @@ import requests misperrors = {'error': 'Error'} mispattributes = {'input': ['vulnerability'], 'format': 'misp_standard'} -moduleinfo = {'version': '1', 'author': 'Christian Studer', 'description': 'An expansion module to enrich a CVE attribute with the vulnerability information.', 'module-type': ['expansion', 'hover']} +moduleinfo = {'version': '1', 'author': 'Christian Studer', + 'description': 'An expansion module to enrich a CVE attribute with the vulnerability information.', + 'module-type': ['expansion', 'hover']} moduleconfig = [] cveapi_url = 'https://cve.circl.lu/api/cve/' @@ -17,7 +19,7 @@ class VulnerabilityParser(): 'id': ('text', 'id'), 'summary': ('text', 'summary'), 'vulnerable_configuration_cpe_2_2': ('text', 'vulnerable_configuration'), 'Modified': ('datetime', 'modified'), 'Published': ('datetime', 'published'), - 'references': ('link', 'references'), 'cvss': ('float', 'cvss')} + 'references': ('link', 'references'), 'cvss': ('float', 'cvss-score')} def get_result(self): event = json.loads(self.misp_event.to_json())['Event']