From f77baec63bfbf130125adda77be6dd0858ce9604 Mon Sep 17 00:00:00 2001 From: Milo Volpicelli Date: Wed, 18 Oct 2023 14:18:29 +0000 Subject: [PATCH 1/8] adds cluster25.py expansion module and entry in expansion/__init__.py --- misp_modules/modules/expansion/__init__.py | 4 +- misp_modules/modules/expansion/cluster25.py | 134 ++++++++++++++++++++ 2 files changed, 136 insertions(+), 2 deletions(-) create mode 100755 misp_modules/modules/expansion/cluster25.py diff --git a/misp_modules/modules/expansion/__init__.py b/misp_modules/modules/expansion/__init__.py index bf6c6db..b323a7f 100644 --- a/misp_modules/modules/expansion/__init__.py +++ b/misp_modules/modules/expansion/__init__.py @@ -4,8 +4,8 @@ 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', 'cve_advanced', 'cpe', 'dns', 'btc_steroids', 'domaintools', 'eupi', - 'eql', 'farsight_passivedns', 'ipasn', 'passivetotal', 'sourcecache', 'virustotal', + 'cluster25', 'countrycode', 'cve', 'cve_advanced', 'cpe', 'dns', 'btc_steroids', 'domaintools', + 'eupi', 'eql', 'farsight_passivedns', 'ipasn', 'passivetotal', 'sourcecache', 'virustotal', 'whois', 'shodan', 'reversedns', 'geoip_asn', 'geoip_city', 'geoip_country', 'wiki', 'iprep', 'threatminer', 'otx', 'threatcrowd', 'vulndb', 'crowdstrike_falcon', 'yara_syntax_validator', 'hashdd', 'onyphe', 'onyphe_full', 'rbl', diff --git a/misp_modules/modules/expansion/cluster25.py b/misp_modules/modules/expansion/cluster25.py new file mode 100755 index 0000000..45dcfa3 --- /dev/null +++ b/misp_modules/modules/expansion/cluster25.py @@ -0,0 +1,134 @@ +import json +import requests +from . import check_input_attribute, standard_error_message +from pymisp import MISPAttribute, MISPEvent + +moduleinfo = {'version': '0.1', + 'author': 'Milo Volpicelli', + 'description': 'Module to query Cluster25CTI', + 'module-type': ['expansion', 'hover']} +moduleconfig = ['api_id', 'apikey', 'base_url'] +misperrors = {'error': 'Error'} +misp_type_in = ['domain', 'email', 'filename', 'md5', 'sha1', 'sha256', 'ip', 'mutex', 'url', 'vulnerability', 'btc', + 'xmr', 'ja3-fingerprint-md5'] +mapping_out = { # mapping between the MISP attributes type and the compatible Cluster25 indicator types. + 'domain': {'type': 'domain', 'to_ids': True}, + 'email': {'type': 'email', 'to_ids': True}, + 'filename': {'type': 'filename', 'to_ids': True}, + 'md5': {'type': 'md5', 'to_ids': True}, + 'sha1': {'type': 'sha1', 'to_ids': True}, + 'sha256': {'type': 'sha256', 'to_ids': True}, + 'ipv4': {'type': 'ip', 'to_ids': True}, + 'ipv6': {'type': 'ip', 'to_ids': True}, + 'mutex': {'type': 'mutex', 'to_ids': True}, + 'url': {'type': 'url', 'to_ids': True}, + 'cve': {'type': 'vulnerability', 'to_ids': True}, + 'btcaddress': {'type': 'btc', 'to_ids': True}, + 'xmraddress': {'type': 'xmr', 'to_ids': True}, + 'ja3': {'type': 'ja3-fingerprint-md5', 'to_ids': True}, +} +misp_type_out = [item['type'] for item in mapping_out.values()] +misp_attributes = {'input': misp_type_in, 'format': 'misp_standard'} + + +def handler(q=False): + if q is False: + return False + request = json.loads(q) + # validate Cluster25 params + if request.get('config'): + if request['config'].get('apikey') is None: + misperrors['error'] = 'Cluster25 apikey is missing' + return misperrors + if request['config'].get('api_id') is None: + misperrors['error'] = 'Cluster25 api_id is missing' + return misperrors + if request['config'].get('base_url') is None: + misperrors['error'] = 'Cluster25 base_url is missing' + return misperrors + + # validate attribute + if not request.get('attribute') or not check_input_attribute(request['attribute']): + return {'error': f'{standard_error_message}, which should contain at least a type, a value and an uuid.'} + attribute = request.get('attribute') + if not any(input_type == attribute.get('type') for input_type in misp_type_in): + return {'error': 'Unsupported attribute type.'} + + client = Cluster25CTI(request['config']['api_id'], request['config']['apikey'], request['config']['base_url']) + + attribute = MISPAttribute() + attribute.from_dict(**request.get('attribute')) + r = {"results": []} + valid_type = False + + try: + for k in misp_type_in: + if attribute.type == k: + # map the MISP type to the Cluster25 type + r['results'].append(lookup_indicator(client, attribute)) + valid_type = True + except Exception as e: + return {'error': f"{e}"} + + if not valid_type: + misperrors['error'] = "Unsupported attributes type" + return misperrors + return {'results': r.get('results').pop()} + + +def lookup_indicator(client, ref_attribute): + result = client.search_indicators(ref_attribute.value) + misp_event = MISPEvent() + misp_event.add_attribute(**ref_attribute) + + for item in result: + if mapping_out.get(item.get('type')): + r = mapping_out[item.get('type')].copy() + r['value'] = item + attribute = MISPAttribute() + attribute.from_dict(**r) + misp_event.add_attribute(**attribute) + + event = json.loads(misp_event.to_json()) + return {'Object': event.get('Object', []), 'Attribute': event.get('Attribute', [])} + + +def introspection(): + return misp_attributes + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo + + +class Cluster25CTI: + def __init__(self, customer_id=None, customer_key=None, base_url=None): + self.client_id = customer_id + self.client_secret = customer_key + self.base_url = base_url + self.current_token = self._get_cluster25_token() + + def _get_cluster25_token(self) -> str: + payload = {"client_id": self.client_id, "client_secret": self.client_secret} + r = requests.post(url=f"{self.base_url}/token", json=payload, headers={"Content-Type": "application/json"}) + + if r.status_code != 200: + raise Exception( + f"Unable to retrieve the token from C25 platform, status {r.status_code}" + ) + return r.json()["data"]["token"] + + def search_indicators(self, indicator_type): + headers = {"Authorization": f"Bearer {self.current_token}"} + params = {'type': indicator_type, 'include_info': True} + r = requests.get(url=f"{self.base_url}/indicators", params=params, headers=headers) + + if r.status_code != 200: + raise Exception( + f"Unable to retrieve the indicators from C25 platform, status {r.status_code}" + ) + return r.json()["data"] + + + From 4c7637237f73f35f5643289d5603f423e6545f58 Mon Sep 17 00:00:00 2001 From: Milo Volpicelli Date: Fri, 20 Oct 2023 08:37:21 +0000 Subject: [PATCH 2/8] renamed cluster25.py to cluster25_expand.py, module implementation --- misp_modules/modules/expansion/__init__.py | 2 +- .../modules/expansion/{cluster25.py => cluster25_expand.py} | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename misp_modules/modules/expansion/{cluster25.py => cluster25_expand.py} (97%) diff --git a/misp_modules/modules/expansion/__init__.py b/misp_modules/modules/expansion/__init__.py index b323a7f..ad29eff 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', - 'cluster25', 'countrycode', 'cve', 'cve_advanced', 'cpe', 'dns', 'btc_steroids', 'domaintools', + 'cluster25_expand', 'countrycode', 'cve', 'cve_advanced', 'cpe', 'dns', 'btc_steroids', 'domaintools', 'eupi', 'eql', 'farsight_passivedns', 'ipasn', 'passivetotal', 'sourcecache', 'virustotal', 'whois', 'shodan', 'reversedns', 'geoip_asn', 'geoip_city', 'geoip_country', 'wiki', 'iprep', 'threatminer', 'otx', 'threatcrowd', 'vulndb', 'crowdstrike_falcon', diff --git a/misp_modules/modules/expansion/cluster25.py b/misp_modules/modules/expansion/cluster25_expand.py similarity index 97% rename from misp_modules/modules/expansion/cluster25.py rename to misp_modules/modules/expansion/cluster25_expand.py index 45dcfa3..6828e05 100755 --- a/misp_modules/modules/expansion/cluster25.py +++ b/misp_modules/modules/expansion/cluster25_expand.py @@ -1,5 +1,6 @@ import json import requests +from typing import List from . import check_input_attribute, standard_error_message from pymisp import MISPAttribute, MISPEvent @@ -9,7 +10,7 @@ moduleinfo = {'version': '0.1', 'module-type': ['expansion', 'hover']} moduleconfig = ['api_id', 'apikey', 'base_url'] misperrors = {'error': 'Error'} -misp_type_in = ['domain', 'email', 'filename', 'md5', 'sha1', 'sha256', 'ip', 'mutex', 'url', 'vulnerability', 'btc', +misp_type_in = ['domain', 'email', 'filename', 'md5', 'sha1', 'sha256', 'ip', 'url', 'vulnerability', 'btc', 'xmr', 'ja3-fingerprint-md5'] mapping_out = { # mapping between the MISP attributes type and the compatible Cluster25 indicator types. 'domain': {'type': 'domain', 'to_ids': True}, @@ -20,7 +21,6 @@ mapping_out = { # mapping between the MISP attributes type and the compatible C 'sha256': {'type': 'sha256', 'to_ids': True}, 'ipv4': {'type': 'ip', 'to_ids': True}, 'ipv6': {'type': 'ip', 'to_ids': True}, - 'mutex': {'type': 'mutex', 'to_ids': True}, 'url': {'type': 'url', 'to_ids': True}, 'cve': {'type': 'vulnerability', 'to_ids': True}, 'btcaddress': {'type': 'btc', 'to_ids': True}, @@ -119,7 +119,7 @@ class Cluster25CTI: ) return r.json()["data"]["token"] - def search_indicators(self, indicator_type): + def search_indicators(self, indicator_type) -> List[dict]: headers = {"Authorization": f"Bearer {self.current_token}"} params = {'type': indicator_type, 'include_info': True} r = requests.get(url=f"{self.base_url}/indicators", params=params, headers=headers) From a4893d997d9208b5670dc1fc045108900e740acb Mon Sep 17 00:00:00 2001 From: Milo Volpicelli Date: Fri, 20 Oct 2023 12:36:22 +0000 Subject: [PATCH 3/8] adds cluster25 import module --- misp_modules/modules/import_mod/__init__.py | 3 +- .../modules/import_mod/cluster25_import.py | 133 ++++++++++++++++++ 2 files changed, 135 insertions(+), 1 deletion(-) create mode 100755 misp_modules/modules/import_mod/cluster25_import.py diff --git a/misp_modules/modules/import_mod/__init__.py b/misp_modules/modules/import_mod/__init__.py index 2b3e755..03c2e22 100644 --- a/misp_modules/modules/import_mod/__init__.py +++ b/misp_modules/modules/import_mod/__init__.py @@ -16,5 +16,6 @@ __all__ = [ 'cof2misp', 'joe_import', 'taxii21', - 'url_import' + 'url_import', + 'cluster25_import' ] diff --git a/misp_modules/modules/import_mod/cluster25_import.py b/misp_modules/modules/import_mod/cluster25_import.py new file mode 100755 index 0000000..61c1867 --- /dev/null +++ b/misp_modules/modules/import_mod/cluster25_import.py @@ -0,0 +1,133 @@ +import json +import requests +from typing import List +from . import standard_error_message +from pymisp import MISPAttribute, MISPEvent + +moduleinfo = {'version': '0.1', + 'author': 'Milo Volpicelli', + 'description': 'Module to query and import indicators from Cluster25CTI', + 'module-type': ['import']} +moduleconfig = ['api_id', 'apikey', 'base_url'] +misperrors = {'error': 'Error'} +misp_type_in = ['domain', 'email', 'filename', 'md5', 'sha1', 'sha256', 'ip', 'url', 'vulnerability', 'btc', + 'xmr', 'ja3-fingerprint-md5'] +mapping_out = { # mapping between the MISP attributes type and the compatible Cluster25 indicator types. + 'domain': {'type': 'domain', 'to_ids': True}, + 'email': {'type': 'email', 'to_ids': True}, + 'filename': {'type': 'filename', 'to_ids': True}, + 'md5': {'type': 'md5', 'to_ids': True}, + 'sha1': {'type': 'sha1', 'to_ids': True}, + 'sha256': {'type': 'sha256', 'to_ids': True}, + 'ipv4': {'type': 'ip', 'to_ids': True}, + 'ipv6': {'type': 'ip', 'to_ids': True}, + 'url': {'type': 'url', 'to_ids': True}, + 'cve': {'type': 'vulnerability', 'to_ids': True}, + 'btcaddress': {'type': 'btc', 'to_ids': True}, + 'xmraddress': {'type': 'xmr', 'to_ids': True}, + 'ja3': {'type': 'ja3-fingerprint-md5', 'to_ids': True}, +} +misp_type_out = [item['type'] for item in mapping_out.values()] +misp_attributes = {'input': misp_type_in, 'format': 'misp_standard'} + + +def handler(q=False): + if q is False: + return False + request = json.loads(q) + # validate Cluster25 params + if request.get('config'): + if request['config'].get('apikey') is None: + misperrors['error'] = 'Cluster25 apikey is missing' + return misperrors + if request['config'].get('api_id') is None: + misperrors['error'] = 'Cluster25 api_id is missing' + return misperrors + if request['config'].get('base_url') is None: + misperrors['error'] = 'Cluster25 base_url is missing' + return misperrors + + # validate attribute + if not request.get('params') or not not request.get('params', {}).get('type'): + return {'error': f'{standard_error_message}, which should contain a type'} + attribute = request.get('params') + if not any(input_type == attribute.get('type') for input_type in misp_type_in): + return {'error': 'Unsupported attribute type.'} + + client = Cluster25CTI(request['config']['api_id'], request['config']['apikey'], request['config']['base_url']) + + r = {"results": []} + valid_type = False + + try: + for k in misp_type_in: + if attribute.type == k: + # map the MISP type to the Cluster25 type + r['results'].append(lookup_indicator(client, attribute)) + valid_type = True + except Exception as e: + return {'error': f"{e}"} + + if not valid_type: + misperrors['error'] = "Unsupported attributes type" + return misperrors + return {'results': r.get('results').pop()} + + +def lookup_indicator(client, ref_attribute): + limit = ref_attribute.limit + if not limit: + limit = 1000 + result = client.search_indicators(ref_attribute.type, limit) + misp_event = MISPEvent() + + for item in result: + if mapping_out.get(item.get('type')): + r = mapping_out[item.get('type')].copy() + r['value'] = item + attribute = MISPAttribute() + attribute.from_dict(**r) + misp_event.add_attribute(**attribute) + + event = json.loads(misp_event.to_json()) + return {'Object': event.get('Object', []), 'Attribute': event.get('Attribute', [])} + + +def introspection(): + return misp_attributes + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo + + +class Cluster25CTI: + def __init__(self, customer_id=None, customer_key=None, base_url=None): + self.client_id = customer_id + self.client_secret = customer_key + self.base_url = base_url + self.current_token = self._get_cluster25_token() + + def _get_cluster25_token(self) -> str: + payload = {"client_id": self.client_id, "client_secret": self.client_secret} + r = requests.post(url=f"{self.base_url}/token", json=payload, headers={"Content-Type": "application/json"}) + + if r.status_code != 200: + raise Exception( + f"Unable to retrieve the token from C25 platform, status {r.status_code}" + ) + return r.json()["data"]["token"] + + def search_indicators(self, indicator_type, limit) -> List[dict]: + headers = {"Authorization": f"Bearer {self.current_token}"} + params = {'type': indicator_type, 'limit': limit} + r = requests.get(url=f"{self.base_url}/indicators", params=params, headers=headers) + + if r.status_code != 200: + raise Exception( + f"Unable to retrieve the indicators from C25 platform, status {r.status_code}" + ) + return r.json()["data"] + + From 0b167df5b06eaa0dd8173127db2e69feacfc63db Mon Sep 17 00:00:00 2001 From: Milo Volpicelli Date: Fri, 20 Oct 2023 13:22:26 +0000 Subject: [PATCH 4/8] actual expand implementation --- .../modules/expansion/cluster25_expand.py | 76 +++++++------------ 1 file changed, 26 insertions(+), 50 deletions(-) diff --git a/misp_modules/modules/expansion/cluster25_expand.py b/misp_modules/modules/expansion/cluster25_expand.py index 6828e05..ace8d8b 100755 --- a/misp_modules/modules/expansion/cluster25_expand.py +++ b/misp_modules/modules/expansion/cluster25_expand.py @@ -1,7 +1,8 @@ import json import requests +import time from typing import List -from . import check_input_attribute, standard_error_message +from . import standard_error_message from pymisp import MISPAttribute, MISPEvent moduleinfo = {'version': '0.1', @@ -47,47 +48,29 @@ def handler(q=False): misperrors['error'] = 'Cluster25 base_url is missing' return misperrors - # validate attribute - if not request.get('attribute') or not check_input_attribute(request['attribute']): - return {'error': f'{standard_error_message}, which should contain at least a type, a value and an uuid.'} - attribute = request.get('attribute') - if not any(input_type == attribute.get('type') for input_type in misp_type_in): - return {'error': 'Unsupported attribute type.'} + # validate params + if not request.get('params') or not request.get('params', {}).get('value'): + return {'error': f'{standard_error_message}, which should contain a value.'} client = Cluster25CTI(request['config']['api_id'], request['config']['apikey'], request['config']['base_url']) - attribute = MISPAttribute() - attribute.from_dict(**request.get('attribute')) - r = {"results": []} - valid_type = False - - try: - for k in misp_type_in: - if attribute.type == k: - # map the MISP type to the Cluster25 type - r['results'].append(lookup_indicator(client, attribute)) - valid_type = True - except Exception as e: - return {'error': f"{e}"} - - if not valid_type: - misperrors['error'] = "Unsupported attributes type" - return misperrors - return {'results': r.get('results').pop()} + return {'results': lookup_indicator(client, request.get('params'))} -def lookup_indicator(client, ref_attribute): - result = client.search_indicators(ref_attribute.value) +def lookup_indicator(client, indicator): + + result = client.investigate(indicator) + misp_event = MISPEvent() - misp_event.add_attribute(**ref_attribute) + if result.get('error'): + return result - for item in result: - if mapping_out.get(item.get('type')): - r = mapping_out[item.get('type')].copy() - r['value'] = item - attribute = MISPAttribute() - attribute.from_dict(**r) - misp_event.add_attribute(**attribute) + if mapping_out.get(result.get('indicator_type')): + r = mapping_out[result.get('indicator_type')].copy() + r['value'] = result + attribute = MISPAttribute() + attribute.from_dict(**r) + misp_event.add_attribute(**attribute) event = json.loads(misp_event.to_json()) return {'Object': event.get('Object', []), 'Attribute': event.get('Attribute', [])} @@ -108,27 +91,20 @@ class Cluster25CTI: self.client_secret = customer_key self.base_url = base_url self.current_token = self._get_cluster25_token() + self.headers = {"Authorization": f"Bearer {self.current_token}"} - def _get_cluster25_token(self) -> str: + def _get_cluster25_token(self): payload = {"client_id": self.client_id, "client_secret": self.client_secret} r = requests.post(url=f"{self.base_url}/token", json=payload, headers={"Content-Type": "application/json"}) - if r.status_code != 200: - raise Exception( - f"Unable to retrieve the token from C25 platform, status {r.status_code}" - ) + return {'error': f"Unable to retrieve the token from C25 platform, status {r.status_code}"} return r.json()["data"]["token"] - def search_indicators(self, indicator_type) -> List[dict]: - headers = {"Authorization": f"Bearer {self.current_token}"} - params = {'type': indicator_type, 'include_info': True} - r = requests.get(url=f"{self.base_url}/indicators", params=params, headers=headers) - + def investigate(self, indicator) -> dict: + params = {'indicator': indicator.get('value')} + r = requests.get(url=f"{self.base_url}/investigate", params=params, headers=self.headers) if r.status_code != 200: - raise Exception( - f"Unable to retrieve the indicators from C25 platform, status {r.status_code}" - ) + return{'error': f"Unable to retrieve investigate result for indicator '{indicator.get('value')}' " + f"from C25 platform, status {r.status_code}"} return r.json()["data"] - - From ce7d1175e7c33617d37298ce6bd85b9a7127a215 Mon Sep 17 00:00:00 2001 From: Milo Volpicelli Date: Thu, 26 Oct 2023 15:33:16 +0000 Subject: [PATCH 5/8] remove addition of cluster25 import module --- misp_modules/modules/import_mod/__init__.py | 3 +- .../modules/import_mod/cluster25_import.py | 133 ------------------ 2 files changed, 1 insertion(+), 135 deletions(-) delete mode 100755 misp_modules/modules/import_mod/cluster25_import.py diff --git a/misp_modules/modules/import_mod/__init__.py b/misp_modules/modules/import_mod/__init__.py index 03c2e22..2b3e755 100644 --- a/misp_modules/modules/import_mod/__init__.py +++ b/misp_modules/modules/import_mod/__init__.py @@ -16,6 +16,5 @@ __all__ = [ 'cof2misp', 'joe_import', 'taxii21', - 'url_import', - 'cluster25_import' + 'url_import' ] diff --git a/misp_modules/modules/import_mod/cluster25_import.py b/misp_modules/modules/import_mod/cluster25_import.py deleted file mode 100755 index 61c1867..0000000 --- a/misp_modules/modules/import_mod/cluster25_import.py +++ /dev/null @@ -1,133 +0,0 @@ -import json -import requests -from typing import List -from . import standard_error_message -from pymisp import MISPAttribute, MISPEvent - -moduleinfo = {'version': '0.1', - 'author': 'Milo Volpicelli', - 'description': 'Module to query and import indicators from Cluster25CTI', - 'module-type': ['import']} -moduleconfig = ['api_id', 'apikey', 'base_url'] -misperrors = {'error': 'Error'} -misp_type_in = ['domain', 'email', 'filename', 'md5', 'sha1', 'sha256', 'ip', 'url', 'vulnerability', 'btc', - 'xmr', 'ja3-fingerprint-md5'] -mapping_out = { # mapping between the MISP attributes type and the compatible Cluster25 indicator types. - 'domain': {'type': 'domain', 'to_ids': True}, - 'email': {'type': 'email', 'to_ids': True}, - 'filename': {'type': 'filename', 'to_ids': True}, - 'md5': {'type': 'md5', 'to_ids': True}, - 'sha1': {'type': 'sha1', 'to_ids': True}, - 'sha256': {'type': 'sha256', 'to_ids': True}, - 'ipv4': {'type': 'ip', 'to_ids': True}, - 'ipv6': {'type': 'ip', 'to_ids': True}, - 'url': {'type': 'url', 'to_ids': True}, - 'cve': {'type': 'vulnerability', 'to_ids': True}, - 'btcaddress': {'type': 'btc', 'to_ids': True}, - 'xmraddress': {'type': 'xmr', 'to_ids': True}, - 'ja3': {'type': 'ja3-fingerprint-md5', 'to_ids': True}, -} -misp_type_out = [item['type'] for item in mapping_out.values()] -misp_attributes = {'input': misp_type_in, 'format': 'misp_standard'} - - -def handler(q=False): - if q is False: - return False - request = json.loads(q) - # validate Cluster25 params - if request.get('config'): - if request['config'].get('apikey') is None: - misperrors['error'] = 'Cluster25 apikey is missing' - return misperrors - if request['config'].get('api_id') is None: - misperrors['error'] = 'Cluster25 api_id is missing' - return misperrors - if request['config'].get('base_url') is None: - misperrors['error'] = 'Cluster25 base_url is missing' - return misperrors - - # validate attribute - if not request.get('params') or not not request.get('params', {}).get('type'): - return {'error': f'{standard_error_message}, which should contain a type'} - attribute = request.get('params') - if not any(input_type == attribute.get('type') for input_type in misp_type_in): - return {'error': 'Unsupported attribute type.'} - - client = Cluster25CTI(request['config']['api_id'], request['config']['apikey'], request['config']['base_url']) - - r = {"results": []} - valid_type = False - - try: - for k in misp_type_in: - if attribute.type == k: - # map the MISP type to the Cluster25 type - r['results'].append(lookup_indicator(client, attribute)) - valid_type = True - except Exception as e: - return {'error': f"{e}"} - - if not valid_type: - misperrors['error'] = "Unsupported attributes type" - return misperrors - return {'results': r.get('results').pop()} - - -def lookup_indicator(client, ref_attribute): - limit = ref_attribute.limit - if not limit: - limit = 1000 - result = client.search_indicators(ref_attribute.type, limit) - misp_event = MISPEvent() - - for item in result: - if mapping_out.get(item.get('type')): - r = mapping_out[item.get('type')].copy() - r['value'] = item - attribute = MISPAttribute() - attribute.from_dict(**r) - misp_event.add_attribute(**attribute) - - event = json.loads(misp_event.to_json()) - return {'Object': event.get('Object', []), 'Attribute': event.get('Attribute', [])} - - -def introspection(): - return misp_attributes - - -def version(): - moduleinfo['config'] = moduleconfig - return moduleinfo - - -class Cluster25CTI: - def __init__(self, customer_id=None, customer_key=None, base_url=None): - self.client_id = customer_id - self.client_secret = customer_key - self.base_url = base_url - self.current_token = self._get_cluster25_token() - - def _get_cluster25_token(self) -> str: - payload = {"client_id": self.client_id, "client_secret": self.client_secret} - r = requests.post(url=f"{self.base_url}/token", json=payload, headers={"Content-Type": "application/json"}) - - if r.status_code != 200: - raise Exception( - f"Unable to retrieve the token from C25 platform, status {r.status_code}" - ) - return r.json()["data"]["token"] - - def search_indicators(self, indicator_type, limit) -> List[dict]: - headers = {"Authorization": f"Bearer {self.current_token}"} - params = {'type': indicator_type, 'limit': limit} - r = requests.get(url=f"{self.base_url}/indicators", params=params, headers=headers) - - if r.status_code != 200: - raise Exception( - f"Unable to retrieve the indicators from C25 platform, status {r.status_code}" - ) - return r.json()["data"] - - From a4bcc15db0251dcf5e7933b71376139bb6a41b09 Mon Sep 17 00:00:00 2001 From: Milo Volpicelli Date: Thu, 26 Oct 2023 15:47:22 +0000 Subject: [PATCH 6/8] enriches with c25 MISP objects --- .../modules/expansion/cluster25_expand.py | 140 +++++++++++++++--- 1 file changed, 116 insertions(+), 24 deletions(-) diff --git a/misp_modules/modules/expansion/cluster25_expand.py b/misp_modules/modules/expansion/cluster25_expand.py index ace8d8b..f777e3b 100755 --- a/misp_modules/modules/expansion/cluster25_expand.py +++ b/misp_modules/modules/expansion/cluster25_expand.py @@ -1,9 +1,8 @@ import json import requests -import time -from typing import List -from . import standard_error_message -from pymisp import MISPAttribute, MISPEvent +import uuid +from . import check_input_attribute, standard_error_message +from pymisp import MISPAttribute, MISPEvent, MISPObject moduleinfo = {'version': '0.1', 'author': 'Milo Volpicelli', @@ -11,17 +10,19 @@ moduleinfo = {'version': '0.1', 'module-type': ['expansion', 'hover']} moduleconfig = ['api_id', 'apikey', 'base_url'] misperrors = {'error': 'Error'} -misp_type_in = ['domain', 'email', 'filename', 'md5', 'sha1', 'sha256', 'ip', 'url', 'vulnerability', 'btc', - 'xmr', 'ja3-fingerprint-md5'] +misp_type_in = ['domain', 'email-src', 'email-dst', 'filename', 'md5', 'sha1', 'sha256', 'ip-src', 'ip-dst', 'url', + 'vulnerability', 'btc', 'xmr', 'ja3-fingerprint-md5'] + mapping_out = { # mapping between the MISP attributes type and the compatible Cluster25 indicator types. 'domain': {'type': 'domain', 'to_ids': True}, - 'email': {'type': 'email', 'to_ids': True}, + 'email-src': {'type': 'email-src', 'to_ids': True}, + 'email-dst': {'type': 'email-dst', 'to_ids': True}, 'filename': {'type': 'filename', 'to_ids': True}, 'md5': {'type': 'md5', 'to_ids': True}, 'sha1': {'type': 'sha1', 'to_ids': True}, 'sha256': {'type': 'sha256', 'to_ids': True}, - 'ipv4': {'type': 'ip', 'to_ids': True}, - 'ipv6': {'type': 'ip', 'to_ids': True}, + 'ip-src': {'type': 'ip-src', 'to_ids': True}, + 'ip-dst': {'type': 'ip-dst', 'to_ids': True}, 'url': {'type': 'url', 'to_ids': True}, 'cve': {'type': 'vulnerability', 'to_ids': True}, 'btcaddress': {'type': 'btc', 'to_ids': True}, @@ -48,32 +49,124 @@ def handler(q=False): misperrors['error'] = 'Cluster25 base_url is missing' return misperrors - # validate params - if not request.get('params') or not request.get('params', {}).get('value'): - return {'error': f'{standard_error_message}, which should contain a value.'} + # validate attribute + if not request.get('attribute') or not check_input_attribute(request['attribute']): + return {'error': f'{standard_error_message}, which should contain at least a type, a value and an uuid.'} + attribute = request.get('attribute') + if not any(input_type == attribute.get('type') for input_type in misp_type_in): + return {'error': 'Unsupported attribute type.'} client = Cluster25CTI(request['config']['api_id'], request['config']['apikey'], request['config']['base_url']) - return {'results': lookup_indicator(client, request.get('params'))} + return lookup_indicator(client, request.get('attribute')) -def lookup_indicator(client, indicator): +def format_content(content): + if isinstance(content, str) or isinstance(content, bool) or isinstance(content, int): + return content + ret = "" + tmp_ret = [] + if content is None: + return ret + is_dict = isinstance(content, dict) + is_list = isinstance(content, list) + for index, key in enumerate(content): + if is_dict: + if isinstance(content[key], dict): + ret = format_content(content[key]) - result = client.investigate(indicator) + elif isinstance(content[key], list): + for list_item in content[key]: + tmp_ret.append(format_content(list_item)) + else: + tmp_ret.append(f"{key}: {content[key]}") + elif is_list: + if isinstance(content[index], str): + ret = ", ".join(content) + else: + ret = format_content(content) + if tmp_ret: + ret = " ".join(tmp_ret) + return ret - misp_event = MISPEvent() + +def lookup_indicator(client, attr): + + result = client.investigate(attr) if result.get('error'): return result + misp_event = MISPEvent() + attribute = MISPAttribute() + attribute.from_dict(**attr) + misp_event.add_attribute(**attribute) - if mapping_out.get(result.get('indicator_type')): - r = mapping_out[result.get('indicator_type')].copy() - r['value'] = result - attribute = MISPAttribute() - attribute.from_dict(**r) - misp_event.add_attribute(**attribute) + misp_object_g = MISPObject('c25_generic_info') + misp_object_g.template_uuid = uuid.uuid4() + misp_object_g.description = 'c25_generic_info' + setattr(misp_object_g, 'meta-category', 'network') + + misp_objects = [] + for ind, entry in enumerate(result): + if isinstance(result[entry], dict): + tmp_obj = MISPObject(f"c25_{entry}") + tmp_obj.template_uuid = uuid.uuid4() + tmp_obj.description = f"c25_{entry}" + setattr(tmp_obj, 'meta-category', 'network') + tmp_obj.add_reference(attribute['uuid'], 'related-to') + for key in result[entry]: + if isinstance(result[entry][key], dict): + for index, key2 in enumerate(result[entry][key]): + if result[entry][key][key2]: + tmp_obj.add_attribute(f"{entry}_{key}_{key2}", **{'type': 'text', 'value': format_content( + result[entry][key][key2])}) + + elif isinstance(result[entry][key], list): + for index, key2 in enumerate(result[entry][key]): + if isinstance(key2, dict): + tmp_obj_2 = MISPObject(f"c25_{entry}_{key}_{index+1}") + tmp_obj_2.template_uuid = uuid.uuid4() + tmp_obj_2.description = f"c25_{entry}_{key}" + setattr(tmp_obj_2, 'meta-category', 'network') + tmp_obj_2.add_reference(attribute['uuid'], 'related-to') + for k in key2: + if key2[k]: + tmp_obj_2.add_attribute(k, **{'type': 'text', 'value': format_content(key2[k])}) + misp_objects.append(tmp_obj_2) + elif key2 is not None: + tmp_obj.add_attribute(f"{entry}_{key}", **{'type': 'text', 'value': format_content(key2)}) + elif result[entry][key] is not None: + tmp_obj.add_attribute(key, **{'type': 'text', 'value': result[entry][key]}) + + if tmp_obj.attributes: + misp_objects.append(tmp_obj) + + elif isinstance(result[entry], list): + for index, key in enumerate(result[entry]): + if isinstance(key, dict): + tmp_obj = MISPObject(f"c25_{entry}_{index+1}") + tmp_obj.template_uuid = uuid.uuid4() + tmp_obj.description = f"c25_{entry}_{index+1}" + setattr(tmp_obj, 'meta-category', 'network') + tmp_obj.add_reference(attribute['uuid'], 'related-to') + for key2 in key: + if key[key2]: + tmp_obj.add_attribute(key2, **{'type': 'text', 'value': format_content(key[key2])}) + tmp_obj.add_reference(attribute['uuid'], 'related-to') + misp_objects.append(tmp_obj) + elif key is not None: + misp_object_g.add_attribute(entry, **{'type': 'text', 'value': format_content(key)}) + else: + if result[entry]: + misp_object_g.add_attribute(entry, **{'type': 'text', 'value': result[entry]}) + + misp_object_g.add_reference(attribute['uuid'], 'related-to') + misp_event.add_object(misp_object_g) + for misp_object in misp_objects: + misp_event.add_object(misp_object) event = json.loads(misp_event.to_json()) - return {'Object': event.get('Object', []), 'Attribute': event.get('Attribute', [])} + results = {key: event[key] for key in ('Attribute', 'Object')} + return {'results': results} def introspection(): @@ -107,4 +200,3 @@ class Cluster25CTI: return{'error': f"Unable to retrieve investigate result for indicator '{indicator.get('value')}' " f"from C25 platform, status {r.status_code}"} return r.json()["data"] - From 52f53f81d0735aacc22f3693b1630ce0965f1bf7 Mon Sep 17 00:00:00 2001 From: Milo Volpicelli Date: Tue, 7 Nov 2023 15:23:33 +0000 Subject: [PATCH 7/8] cluster25_expand: handles related items and more --- .../modules/expansion/cluster25_expand.py | 76 +++++++++++++------ 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/misp_modules/modules/expansion/cluster25_expand.py b/misp_modules/modules/expansion/cluster25_expand.py index f777e3b..5da8c47 100755 --- a/misp_modules/modules/expansion/cluster25_expand.py +++ b/misp_modules/modules/expansion/cluster25_expand.py @@ -10,7 +10,7 @@ moduleinfo = {'version': '0.1', 'module-type': ['expansion', 'hover']} moduleconfig = ['api_id', 'apikey', 'base_url'] misperrors = {'error': 'Error'} -misp_type_in = ['domain', 'email-src', 'email-dst', 'filename', 'md5', 'sha1', 'sha256', 'ip-src', 'ip-dst', 'url', +misp_type_in = ['domain', 'email-src', 'email-dst', 'filename', 'md5', 'sha1', 'sha256', 'ip-src', 'ip-dst', 'url', 'vulnerability', 'btc', 'xmr', 'ja3-fingerprint-md5'] mapping_out = { # mapping between the MISP attributes type and the compatible Cluster25 indicator types. @@ -62,7 +62,7 @@ def handler(q=False): def format_content(content): - if isinstance(content, str) or isinstance(content, bool) or isinstance(content, int): + if isinstance(content, str) or isinstance(content, bool) or isinstance(content, int) or isinstance(content, float): return content ret = "" tmp_ret = [] @@ -74,7 +74,6 @@ def format_content(content): if is_dict: if isinstance(content[key], dict): ret = format_content(content[key]) - elif isinstance(content[key], list): for list_item in content[key]: tmp_ret.append(format_content(list_item)) @@ -91,7 +90,6 @@ def format_content(content): def lookup_indicator(client, attr): - result = client.investigate(attr) if result.get('error'): return result @@ -115,25 +113,53 @@ def lookup_indicator(client, attr): tmp_obj.add_reference(attribute['uuid'], 'related-to') for key in result[entry]: if isinstance(result[entry][key], dict): - for index, key2 in enumerate(result[entry][key]): - if result[entry][key][key2]: - tmp_obj.add_attribute(f"{entry}_{key}_{key2}", **{'type': 'text', 'value': format_content( - result[entry][key][key2])}) + for index, item in enumerate(result[entry][key]): + if result[entry][key][item]: + tmp_obj.add_attribute(f"{entry}_{key}_{item}", **{'type': 'text', 'value': format_content( + result[entry][key][item])}) elif isinstance(result[entry][key], list): - for index, key2 in enumerate(result[entry][key]): - if isinstance(key2, dict): - tmp_obj_2 = MISPObject(f"c25_{entry}_{key}_{index+1}") + for index, item in enumerate(result[entry][key]): + if isinstance(item, dict): + tmp_obj_2 = MISPObject(f"c25_{entry}_{key}_{index + 1}") tmp_obj_2.template_uuid = uuid.uuid4() tmp_obj_2.description = f"c25_{entry}_{key}" setattr(tmp_obj_2, 'meta-category', 'network') tmp_obj_2.add_reference(attribute['uuid'], 'related-to') - for k in key2: - if key2[k]: - tmp_obj_2.add_attribute(k, **{'type': 'text', 'value': format_content(key2[k])}) + for sub_key in item: + if isinstance(item[sub_key], list): + for sub_item in item[sub_key]: + if isinstance(sub_item, dict): + tmp_obj_3 = MISPObject(f"c25_{entry}_{sub_key}_{index + 1}") + tmp_obj_3.template_uuid = uuid.uuid4() + tmp_obj_3.description = f"c25_{entry}_{sub_key}" + setattr(tmp_obj_3, 'meta-category', 'network') + tmp_obj_3.add_reference(attribute['uuid'], 'related-to') + for sub_sub_key in sub_item: + if isinstance(sub_item[sub_sub_key], list): + for idx, sub_sub_item in enumerate(sub_item[sub_sub_key]): + if sub_sub_item.get("name"): + sub_sub_item = sub_sub_item.get("name") + tmp_obj_3.add_attribute(f"{sub_sub_key}_{idx + 1}", + **{'type': 'text', + 'value': format_content( + sub_sub_item)}) + else: + tmp_obj_3.add_attribute(sub_sub_key, + **{'type': 'text', + 'value': format_content( + sub_item[sub_sub_key])}) + misp_objects.append(tmp_obj_3) + else: + tmp_obj_2.add_attribute(sub_key, **{'type': 'text', + 'value': format_content(sub_item)}) + + elif item[sub_key]: + tmp_obj_2.add_attribute(sub_key, + **{'type': 'text', 'value': format_content(item[sub_key])}) misp_objects.append(tmp_obj_2) - elif key2 is not None: - tmp_obj.add_attribute(f"{entry}_{key}", **{'type': 'text', 'value': format_content(key2)}) + elif item is not None: + tmp_obj.add_attribute(f"{entry}_{key}", **{'type': 'text', 'value': format_content(item)}) elif result[entry][key] is not None: tmp_obj.add_attribute(key, **{'type': 'text', 'value': result[entry][key]}) @@ -143,18 +169,19 @@ def lookup_indicator(client, attr): elif isinstance(result[entry], list): for index, key in enumerate(result[entry]): if isinstance(key, dict): - tmp_obj = MISPObject(f"c25_{entry}_{index+1}") + tmp_obj = MISPObject(f"c25_{entry}_{index + 1}") tmp_obj.template_uuid = uuid.uuid4() - tmp_obj.description = f"c25_{entry}_{index+1}" + tmp_obj.description = f"c25_{entry}_{index + 1}" setattr(tmp_obj, 'meta-category', 'network') tmp_obj.add_reference(attribute['uuid'], 'related-to') - for key2 in key: - if key[key2]: - tmp_obj.add_attribute(key2, **{'type': 'text', 'value': format_content(key[key2])}) + for item in key: + if key[item]: + tmp_obj.add_attribute(item, **{'type': 'text', 'value': format_content(key[item])}) tmp_obj.add_reference(attribute['uuid'], 'related-to') misp_objects.append(tmp_obj) elif key is not None: - misp_object_g.add_attribute(entry, **{'type': 'text', 'value': format_content(key)}) + misp_object_g.add_attribute(f"{entry}_{index + 1}", + **{'type': 'text', 'value': format_content(key)}) else: if result[entry]: misp_object_g.add_attribute(entry, **{'type': 'text', 'value': result[entry]}) @@ -197,6 +224,7 @@ class Cluster25CTI: params = {'indicator': indicator.get('value')} r = requests.get(url=f"{self.base_url}/investigate", params=params, headers=self.headers) if r.status_code != 200: - return{'error': f"Unable to retrieve investigate result for indicator '{indicator.get('value')}' " - f"from C25 platform, status {r.status_code}"} + return {'error': f"Unable to retrieve investigate result for indicator '{indicator.get('value')}' " + f"from C25 platform, status {r.status_code}"} return r.json()["data"] + From 27cec2ecd81b78736d333c18a180cd25c5b5d510 Mon Sep 17 00:00:00 2001 From: Milo Volpicelli Date: Tue, 7 Nov 2023 15:41:46 +0000 Subject: [PATCH 8/8] documentation and logos --- docs/logos/cluster25.png | Bin 0 -> 3697 bytes documentation/logos/cluster25.png | Bin 0 -> 3697 bytes .../website/expansion/cluster25_expand.json | 14 ++++++++++++++ 3 files changed, 14 insertions(+) create mode 100644 docs/logos/cluster25.png create mode 100644 documentation/logos/cluster25.png create mode 100644 documentation/website/expansion/cluster25_expand.json diff --git a/docs/logos/cluster25.png b/docs/logos/cluster25.png new file mode 100644 index 0000000000000000000000000000000000000000..e201ca3d80edbc27abe96f8d7ff1eca68e53e46c GIT binary patch literal 3697 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 DS)#z8 literal 0 HcmV?d00001 diff --git a/documentation/logos/cluster25.png b/documentation/logos/cluster25.png new file mode 100644 index 0000000000000000000000000000000000000000..e201ca3d80edbc27abe96f8d7ff1eca68e53e46c GIT binary patch literal 3697 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 DS)#z8 literal 0 HcmV?d00001 diff --git a/documentation/website/expansion/cluster25_expand.json b/documentation/website/expansion/cluster25_expand.json new file mode 100644 index 0000000..d41c212 --- /dev/null +++ b/documentation/website/expansion/cluster25_expand.json @@ -0,0 +1,14 @@ +{ + "description": "Module to query Cluster25 CTI.", + "logo": "cluster25.png", + "requirements": [ + "A Cluster25 API access (API id & key)" + ], + "input": "An Indicator value of type included in the following list:\n- domain\n- email-src\n- email-dst\n- filename\n- md5\n- sha1\n- sha256\n- ip-src\n- ip-dst\n- url\n- vulnerability\n- btc\n- xmr\n ja3-fingerprint-md5", + "output": "A series of c25 MISP Objects with colletion of attributes mapped from Cluster25 CTI query result.", + "references": [ + "" + ], + "features": "This module takes a MISP attribute value as input to query the Cluster25CTI API. The result is then mapped into compatible MISP Objects and relative attributes.\n" +} +