mirror of https://github.com/MISP/misp-modules
				
				
				
			
		
			
				
	
	
		
			237 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
			
		
		
	
	
			237 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
| import dnsdb2
 | |
| import json
 | |
| from . import check_input_attribute, standard_error_message
 | |
| from datetime import datetime
 | |
| from pymisp import MISPEvent, MISPObject, Distribution
 | |
| 
 | |
| misperrors = {'error': 'Error'}
 | |
| standard_query_input = [
 | |
|     'hostname',
 | |
|     'domain',
 | |
|     'ip-src',
 | |
|     'ip-dst'
 | |
| ]
 | |
| flex_query_input = [
 | |
|     'btc',
 | |
|     'dkim',
 | |
|     'email',
 | |
|     'email-src',
 | |
|     'email-dst',
 | |
|     'domain|ip',
 | |
|     'hex',
 | |
|     'mac-address',
 | |
|     'mac-eui-64',
 | |
|     'other',
 | |
|     'pattern-filename',
 | |
|     'target-email',
 | |
|     'text',
 | |
|     'uri',
 | |
|     'url',
 | |
|     'whois-registrant-email',
 | |
| ]
 | |
| mispattributes = {
 | |
|     'input': standard_query_input + flex_query_input,
 | |
|     'format': 'misp_standard'
 | |
| }
 | |
| moduleinfo = {
 | |
|     'version': '0.5',
 | |
|     'author': 'Christophe Vandeplas',
 | |
|     'description': 'Module to access Farsight DNSDB Passive DNS',
 | |
|     'module-type': ['expansion', 'hover']
 | |
| }
 | |
| moduleconfig = ['apikey', 'server', 'limit', 'flex_queries']
 | |
| 
 | |
| DEFAULT_DNSDB_SERVER = 'https://api.dnsdb.info'
 | |
| DEFAULT_LIMIT = 10
 | |
| DEFAULT_DISTRIBUTION_SETTING = Distribution.your_organisation_only.value
 | |
| TYPE_TO_FEATURE = {
 | |
|     "btc": "Bitcoin address",
 | |
|     "dkim": "domainkeys identified mail",
 | |
|     "domain": "domain name",
 | |
|     "domain|ip": "domain name / IP address",
 | |
|     "hex": "value in hexadecimal format",
 | |
|     "hostname": "hostname",
 | |
|     "mac-address": "MAC address",
 | |
|     "mac-eui-64": "MAC EUI-64 address",
 | |
|     "pattern-filename": "pattern in the name of a file",
 | |
|     "target-email": "attack target email",
 | |
|     "uri": "Uniform Resource Identifier",
 | |
|     "url": "Uniform Resource Locator",
 | |
|     "whois-registrant-email": "email of a domain's registrant"
 | |
| }
 | |
| TYPE_TO_FEATURE.update(
 | |
|     dict.fromkeys(
 | |
|         ("ip-src", "ip-dst"),
 | |
|         "IP address"
 | |
|     )
 | |
| )
 | |
| TYPE_TO_FEATURE.update(
 | |
|     dict.fromkeys(
 | |
|         ("email", "email-src", "email-dst"),
 | |
|         "email address"
 | |
|     )
 | |
| )
 | |
| TYPE_TO_FEATURE.update(
 | |
|     dict.fromkeys(
 | |
|         ("other", "text"),
 | |
|         "text"
 | |
|     )
 | |
| )
 | |
| 
 | |
| 
 | |
| class FarsightDnsdbParser():
 | |
|     def __init__(self, attribute):
 | |
|         self.attribute = attribute
 | |
|         self.misp_event = MISPEvent()
 | |
|         self.misp_event.add_attribute(**attribute)
 | |
|         self.passivedns_mapping = {
 | |
|             'bailiwick': {'type': 'domain', 'object_relation': 'bailiwick'},
 | |
|             'count': {'type': 'counter', 'object_relation': 'count'},
 | |
|             'raw_rdata': {'type': 'text', 'object_relation': 'raw_rdata'},
 | |
|             'rdata': {'type': 'text', 'object_relation': 'rdata'},
 | |
|             'rrname': {'type': 'text', 'object_relation': 'rrname'},
 | |
|             'rrtype': {'type': 'text', 'object_relation': 'rrtype'},
 | |
|             'time_first': {'type': 'datetime', 'object_relation': 'time_first'},
 | |
|             'time_last': {'type': 'datetime', 'object_relation': 'time_last'},
 | |
|             'zone_time_first': {'type': 'datetime', 'object_relation': 'zone_time_first'},
 | |
|             'zone_time_last': {'type': 'datetime', 'object_relation': 'zone_time_last'}
 | |
|         }
 | |
|         self.comment = 'Result from a %s lookup on DNSDB about the %s: %s'
 | |
| 
 | |
|     def parse_passivedns_results(self, query_response):
 | |
|         for query_type, results in query_response.items():
 | |
|             comment = self.comment % (query_type, TYPE_TO_FEATURE[self.attribute['type']], self.attribute['value'])
 | |
|             for result in results:
 | |
|                 passivedns_object = MISPObject('passive-dns')
 | |
|                 passivedns_object.distribution = DEFAULT_DISTRIBUTION_SETTING
 | |
|                 if result.get('rdata') and isinstance(result['rdata'], list):
 | |
|                     for rdata in result.pop('rdata'):
 | |
|                         passivedns_object.add_attribute(**self._parse_attribute(comment, 'rdata', rdata))
 | |
|                 for feature, value in result.items():
 | |
|                     passivedns_object.add_attribute(**self._parse_attribute(comment, feature, value))
 | |
|                 if result.get('time_first'):
 | |
|                     passivedns_object.first_seen = result['time_first']
 | |
|                 if result.get('time_last'):
 | |
|                     passivedns_object.last_seen = result['time_last']
 | |
|                 passivedns_object.add_reference(self.attribute['uuid'], 'related-to')
 | |
|                 self.misp_event.add_object(passivedns_object)
 | |
| 
 | |
|     def get_results(self):
 | |
|         event = json.loads(self.misp_event.to_json())
 | |
|         results = {key: event[key] for key in ('Attribute', 'Object')}
 | |
|         return {'results': results}
 | |
| 
 | |
|     def _parse_attribute(self, comment, feature, value):
 | |
|         attribute = {'value': value, 'comment': comment, 'distribution': DEFAULT_DISTRIBUTION_SETTING}
 | |
|         attribute.update(self.passivedns_mapping[feature])
 | |
|         return attribute
 | |
| 
 | |
| 
 | |
| 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'] = 'Farsight DNSDB apikey is missing'
 | |
|         return misperrors
 | |
|     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['attribute']
 | |
|     if attribute['type'] not in mispattributes['input']:
 | |
|         return {'error': 'Unsupported attributes type'}
 | |
|     config = request['config']
 | |
|     if not config.get('server'):
 | |
|         config['server'] = DEFAULT_DNSDB_SERVER
 | |
|     client_args = {feature: config[feature] for feature in ('apikey', 'server')}
 | |
|     client = dnsdb2.Client(**client_args)
 | |
|     to_query, args = parse_input(attribute, config)
 | |
|     try:
 | |
|         response = to_query(client, *args)
 | |
|     except dnsdb2.DnsdbException as e:
 | |
|         return {'error': e.__str__()}
 | |
|     except dnsdb2.exceptions.QueryError:
 | |
|         return {'error': 'Communication error occurs while executing a query, or the server reports an error due to invalid arguments.'}
 | |
|     if not response:
 | |
|         return {'error': f"Empty results on Farsight DNSDB for the {TYPE_TO_FEATURE[attribute['type']]}: {attribute['value']}."}
 | |
|     parser = FarsightDnsdbParser(attribute)
 | |
|     parser.parse_passivedns_results(response)
 | |
|     return parser.get_results()
 | |
| 
 | |
| 
 | |
| def parse_input(attribute, config):
 | |
|     lookup_args = {
 | |
|         'limit': config['limit'] if config.get('limit') else DEFAULT_LIMIT,
 | |
|         'offset': 0,
 | |
|         'ignore_limited': True,
 | |
|         'humantime': True
 | |
|     }
 | |
|     if attribute.get('first_seen'):
 | |
|         lookup_args['time_first_after'] = parse_timestamp(attribute['first_seen'])
 | |
|     attribute_type = attribute['type']
 | |
|     if attribute_type in flex_query_input:
 | |
|         return flex_queries, (lookup_args, attribute['value'])
 | |
|     flex = add_flex_queries(config.get('flex_queries'))
 | |
|     to_query = lookup_ip if 'ip-' in attribute_type else lookup_name
 | |
|     return to_query, (lookup_args, attribute['value'], flex)
 | |
| 
 | |
| 
 | |
| def parse_timestamp(str_date):
 | |
|     datetime_date = datetime.strptime(str_date, '%Y-%m-%dT%H:%M:%S.%f%z')
 | |
|     return str(int(datetime_date.timestamp()))
 | |
| 
 | |
| 
 | |
| def add_flex_queries(flex):
 | |
|     if not flex:
 | |
|         return False
 | |
|     if flex in ('True', 'true', True, '1', 1):
 | |
|         return True
 | |
|     return False
 | |
| 
 | |
| 
 | |
| def flex_queries(client, lookup_args, name):
 | |
|     response = {}
 | |
|     name = name.replace('@', '.')
 | |
|     for feature in ('rdata', 'rrnames'):
 | |
|         to_call = getattr(client, f'flex_{feature}_regex')
 | |
|         results = list(to_call(name, **lookup_args))
 | |
|         for result in list(to_call(name.replace('.', '\\.'), **lookup_args)):
 | |
|             if result not in results:
 | |
|                 results.append(result)
 | |
|         if results:
 | |
|             response[f'flex_{feature}'] = results
 | |
|     return response
 | |
| 
 | |
| 
 | |
| def lookup_name(client, lookup_args, name, flex):
 | |
|     response = {}
 | |
|     # RRSET = entries in the left-hand side of the domain name related labels
 | |
|     rrset_response = list(client.lookup_rrset(name, **lookup_args))
 | |
|     if rrset_response:
 | |
|         response['rrset'] = rrset_response
 | |
|     # RDATA = entries on the right-hand side of the domain name related labels
 | |
|     rdata_response = list(client.lookup_rdata_name(name, **lookup_args))
 | |
|     if rdata_response:
 | |
|         response['rdata'] = rdata_response
 | |
|     if flex:
 | |
|         response.update(flex_queries(client, lookup_args, name))
 | |
|     return response
 | |
| 
 | |
| 
 | |
| def lookup_ip(client, lookup_args, ip, flex):
 | |
|     response = {}
 | |
|     res = list(client.lookup_rdata_ip(ip, **lookup_args))
 | |
|     if res:
 | |
|         response['rdata'] = res
 | |
|     if flex:
 | |
|         response.update(flex_queries(client, lookup_args, ip))
 | |
|     return response
 | |
| 
 | |
| 
 | |
| def introspection():
 | |
|     return mispattributes
 | |
| 
 | |
| 
 | |
| def version():
 | |
|     moduleinfo['config'] = moduleconfig
 | |
|     return moduleinfo
 |