diff --git a/analyzers/Onyphe/Onyphe_Forward.json b/analyzers/Onyphe/Onyphe_Forward.json new file mode 100644 index 000000000..d4f68a22c --- /dev/null +++ b/analyzers/Onyphe/Onyphe_Forward.json @@ -0,0 +1,16 @@ +{ + "name": "Onyphe_Forward", + "version": "1.0", + "author": "Pierre Baudry, Adrien Barchapt", + "url": "https://github.com/cybernardo/Cortex-Analyzers", + "license": "AGPL-V3", + "description": "Retrieve forward DNS lookup information we have for the given IPv{4,6} address with history of changes.", + "dataTypeList": ["ip"], + "baseConfig": "Onyphe", + "config": { + "check_tlp": true, + "max_tlp": 1, + "service": "forward" + }, + "command": "Onyphe/onyphe_analyzer.py" +} diff --git a/analyzers/Onyphe/Onyphe_Geolocate.json b/analyzers/Onyphe/Onyphe_Geolocate.json new file mode 100644 index 000000000..fef1396d4 --- /dev/null +++ b/analyzers/Onyphe/Onyphe_Geolocate.json @@ -0,0 +1,16 @@ +{ + "name": "Onyphe_Geolocate", + "version": "1.0", + "author": "Pierre Baudry, Adrien Barchapt", + "url": "https://github.com/cybernardo/Cortex-Analyzers", + "license": "AGPL-V3", + "description": "Retrieve geolocation information for the given IPv{4,6} address.", + "dataTypeList": ["ip"], + "baseConfig": "Onyphe", + "config": { + "check_tlp": true, + "max_tlp": 1, + "service": "geolocate" + }, + "command": "Onyphe/onyphe_analyzer.py" +} diff --git a/analyzers/Onyphe/Onyphe_Ports.json b/analyzers/Onyphe/Onyphe_Ports.json new file mode 100644 index 000000000..954e1f65b --- /dev/null +++ b/analyzers/Onyphe/Onyphe_Ports.json @@ -0,0 +1,16 @@ +{ + "name": "Onyphe_Ports", + "version": "1.0", + "author": "Pierre Baudry, Adrien Barchapt", + "url": "https://github.com/cybernardo/Cortex-Analyzers", + "license": "AGPL-V3", + "description": "Retrieve synscan information we have for the given IPv{4,6} address with history of changes.", + "dataTypeList": ["ip"], + "baseConfig": "Onyphe", + "config": { + "check_tlp": true, + "max_tlp": 1, + "service": "ports" + }, + "command": "Onyphe/onyphe_analyzer.py" +} diff --git a/analyzers/Onyphe/Onyphe_Reverse.json b/analyzers/Onyphe/Onyphe_Reverse.json new file mode 100644 index 000000000..176e40f28 --- /dev/null +++ b/analyzers/Onyphe/Onyphe_Reverse.json @@ -0,0 +1,16 @@ +{ + "name": "Onyphe_Reverse", + "version": "1.0", + "author": "Pierre Baudry, Adrien Barchapt", + "url": "https://github.com/cybernardo/Cortex-Analyzers", + "license": "AGPL-V3", + "description": "Retrieve reverse DNS lookup information we have for the given IPv{4,6} address with history of changes.", + "dataTypeList": ["ip"], + "baseConfig": "Onyphe", + "config": { + "check_tlp": true, + "max_tlp": 1, + "service": "reverse" + }, + "command": "Onyphe/onyphe_analyzer.py" +} diff --git a/analyzers/Onyphe/Onyphe_Threats.json b/analyzers/Onyphe/Onyphe_Threats.json new file mode 100644 index 000000000..dc6a8ad04 --- /dev/null +++ b/analyzers/Onyphe/Onyphe_Threats.json @@ -0,0 +1,16 @@ +{ + "name": "Onyphe_Threats", + "version": "1.0", + "author": "Pierre Baudry, Adrien Barchapt", + "url": "https://github.com/CERT-BDF/Cortex-Analyzers", + "license": "AGPL-V3", + "description": "Retrieve Onyphe threats information on an IPv{4,6} address with history.", + "dataTypeList": ["ip"], + "baseConfig": "Onyphe", + "config": { + "check_tlp": true, + "max_tlp": 1, + "service": "threats" + }, + "command": "Onyphe/onyphe_analyzer.py" +} diff --git a/analyzers/Onyphe/onyphe_analyzer.py b/analyzers/Onyphe/onyphe_analyzer.py new file mode 100755 index 000000000..07ed3ae2f --- /dev/null +++ b/analyzers/Onyphe/onyphe_analyzer.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 + +from cortexutils.analyzer import Analyzer +from onyphe_api import Onyphe + + +class OnypheAnalyzer(Analyzer): + def __init__(self): + Analyzer.__init__(self) + self.service = self.getParam( + 'config.service', None, 'Service parameter is missing') + self.onyphe_key = self.getParam( + 'config.key', None, 'Missing Onyphe API key') + self.onyphe_client = None + self.polling_interval = self.getParam('config.polling_interval', 60) + + def summary(self, raw): + taxonomies = [] + namespace = "Onyphe" + if self.service == 'threats': + output_data = {} + for r in raw['threats']['results']: + threatlist = r['threatlist'] + if threatlist not in output_data: + output_data[threatlist] = { + "dates": [], + "subnets": [], + "count": 0 + } + + if r['seen_date'] not in output_data[threatlist]["dates"]: + output_data[threatlist]["dates"].append(r['seen_date']) + output_data[threatlist]["count"] += 1 + if r['subnet'] not in output_data[threatlist]["subnets"]: + output_data[threatlist]["subnets"].append(r['subnet']) + for threatlist, threat_data in output_data.items(): + taxonomies.append(self.build_taxonomy( + 'malicious', namespace, "Threat", "threatlist: {}, event count: {}".format( + threatlist, threat_data['count']))) + + if self.service == 'geolocate': + location = raw['location']['results'][0] + taxonomies.append(self.build_taxonomy( + 'info', namespace, "Geolocate", "country: {}, city: {}".format( + location["country_name"], location["city"]))) + + if self.service == 'ports': + output_data = {} + for r in raw['ports']['results']: + port = r['port'] + if port not in output_data: + output_data[port] = { + "dates": [] + } + if r['seen_date'] not in output_data[port]['dates']: + output_data[port]['dates'].append(r['seen_date']) + for port_number, port_data in output_data.items(): + taxonomies.append(self.build_taxonomy( + 'info', namespace, "Port", "port {} last seen {}".format( + port_number, port_data['dates'][0]))) + + if self.service == 'reverse': + output_data = {} + for r in raw['reverses']['results']: + reverse = r['domain'] + if reverse not in output_data: + output_data[reverse] = { + "dates": [] + } + + if r['seen_date'] not in output_data[reverse]["dates"]: + output_data[reverse]["dates"].append(r['seen_date']) + for reverse, reverse_data in output_data.items(): + taxonomies.append(self.build_taxonomy( + 'info', namespace, "DNS Reverse", "name: {}, last_seen: {}".format( + reverse, reverse_data['dates'][0]))) + + if self.service == 'forward': + output_data = {} + for r in raw['forwards']['results']: + forwarder = r['forward'] + if forwarder not in output_data: + output_data[forwarder] = { + "dates": [] + } + + if r['seen_date'] not in output_data[forwarder]["dates"]: + output_data[forwarder]["dates"].append(r['seen_date']) + for forwarder, forward_data in output_data.items(): + taxonomies.append(self.build_taxonomy( + 'info', namespace, "DNS Forwarder", "forwarder: {}, last_seen: {}".format( + forwarder, forward_data['dates'][0]))) + + return {'taxonomies': taxonomies} + + def run(self): + Analyzer.run(self) + try: + self.onyphe_client = Onyphe(self.onyphe_key) + if self.service == 'threats': + ip = self.getParam('data', None, 'Data is missing') + results = {'threats': self.onyphe_client.threatlist(ip)} + self.report(results) + if self.service == 'ports': + ip = self.getParam('data', None, 'Data is missing') + results = {'ports': self.onyphe_client.synscan(ip)} + self.report(results) + if self.service == 'geolocate': + ip = self.getParam('data', None, 'Data is missing') + results = {'location': self.onyphe_client.geolocate(ip)} + self.report(results) + if self.service == 'reverse': + ip = self.getParam('data', None, 'Data is missing') + results = {'reverses': self.onyphe_client.reverse(ip)} + self.report(results) + if self.service == 'forward': + ip = self.getParam('data', None, 'Data is missing') + results = {'forwards': self.onyphe_client.forward(ip)} + self.report(results) + except Exception: + pass + + +if __name__ == '__main__': + OnypheAnalyzer().run() diff --git a/analyzers/Onyphe/onyphe_api.py b/analyzers/Onyphe/onyphe_api.py new file mode 100644 index 000000000..72573b86e --- /dev/null +++ b/analyzers/Onyphe/onyphe_api.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 + +import ipaddress + +from requests.compat import urljoin, quote_plus +import requests + + +class Onyphe: + """Wrapper around the Onyphe REST API + :param key: The Onyphe API key + :type key: str + """ + + def __init__(self, key: str): + """Intializes the API object + :param key: The Onyphe API key + :type key: str + """ + self.api_key = key + self.base_url = "https://www.onyphe.io" + self._session = requests.Session() + + def _request(self, path: str, query_params: dict={}): + """Specialized wrapper arround the requests module to request data from Onyphe + :param path: The URL path after the onyphe FQDN + :type path: str + :param query_params: The dictionnary of query parameters that gets appended to the URL + :type query_params: str + """ + query_params["apikey"] = self.api_key + url = urljoin(self.base_url, path) + response = self._session.get(url=url, data=query_params) + + if response.status_code == 429: + raise APIRateLimiting(response.text) + try: + response_data = response.json() + except: + raise APIError("Couldn't parse response JSON") + + if response_data["error"] > 0: + raise APIError("got error {}: {}".format( + response_data["error"], response_data["message"])) + + return response_data + + def _request_without_api(self, path: str, query_params: dict={}): + """Specialized wrapper arround the requests module to request data from Onyphe without the api_key(geolocate and myip) + :param path: The URL path after the onyphe FQDN + :type path: str + :param query_params: The dictionnary of query parameters that gets appended to the URL + :type query_params: str + """ + url = urljoin(self.base_url, path) + response = self._session.get(url=url, data=query_params) + + if response.status_code == 429: + raise APIRateLimiting(response.text) + try: + response_data = response.json() + except: + raise APIError("Couldn't parse response JSON") + + if response_data["error"] > 0: + raise APIError("got error {}: {}".format( + response_data["error"], response_data["message"])) + + return response_data + + def myip(self, ip: str): + """This method is open to use. There is need for an API key. + """ + url_path = "/api/myip" + return self._request_without_api(path=url_path) + + def geolocate(self, ip: str): + """Return geolocate information from ip address (Geolocate doesn't need apikey !!) + """ + url_path = "/api/geoloc/{ip}".format(ip=ip) + return self._request_without_api(path=url_path) + + def ip(self, ip: str): + """Return a summary of all information we have for the given IPv{4,6} address. History of changes will not be shown, only latest results. + """ + url_path = "/api/ip/{ip}".format(ip=ip) + return self._request(path=url_path) + + def inetnum(self, ip: str): + """Return inetnum information we have for the given IPv{4,6} address with history of changes. Multiple subnets may match because of delegation mechanisms. We return all of them + """ + url_path = "/api/inetnum/{ip}".format(ip=ip) + return self._request(path=url_path) + + def threatlist(self, ip: str): + """Return threatlist information we have for the given IPv{4,6} address with history of changes + """ + url_path = "/api/threatlist/{ip}".format(ip=ip) + return self._request(path=url_path) + + def pastries(self, ip: str): + """Return pastries information we have for the given IPv{4,6} address with history of changes. + """ + url_path = "/api/pastries/{ip}".format(ip=ip) + return self._request(path=url_path) + + def synscan(self, ip: str): + """Return synscan information we have for the given IPv{4,6} address with history of changes. Multiple synscan entries may match. We return all of them. + """ + url_path = "/api/synscan/{ip}".format(ip=ip) + return self._request(path=url_path) + + def datascan(self, search: str): + """Return datascan information we have for the given IPv{4,6} address or string with history of changes + """ + url_path = "/api/datascan/{search}".format(search=search) + return self._request(path=url_path) + + def reverse(self, search: str): + """Return reverse DNS lookup information we have for the given IPv{4,6} address with history of changes. Multiple reverse DNS entries may match. We return all of them. + """ + url_path = "/api/reverse/{search}".format(search=search) + return self._request(path=url_path) + + def forward(self, search: str): + """Return forward DNS lookup information we have for the given IPv{4,6} address with history of changes. Multiple forward DNS entries may match. We return all of them. + """ + url_path = "/api/forward/{search}".format(search=search) + return self._request(path=url_path) + + +class APIError(Exception): + """This exception gets raised when the returned error code is non-zero positive""" + + def __init__(self, value): + self.value = value + + def __str__(self): + return self.value + + +class APIRateLimiting(Exception): + """This exception gets raised when the 429 HTTP code is returned""" + + def __init__(self, value): + self.value = value + + def __str__(self): + return self.value diff --git a/analyzers/Onyphe/requirements.txt b/analyzers/Onyphe/requirements.txt new file mode 100644 index 000000000..6aabc3cfa --- /dev/null +++ b/analyzers/Onyphe/requirements.txt @@ -0,0 +1,2 @@ +cortexutils +requests