2020-11-12 16:01:14 +01:00
import dnsdb2
2017-12-05 16:41:41 +01:00
import json
2020-11-03 19:30:09 +01:00
from . import check_input_attribute , standard_error_message
2021-03-29 20:09:29 +02:00
from datetime import datetime
2021-05-12 18:12:25 +02:00
from pymisp import MISPEvent , MISPObject , Distribution
2017-12-05 16:41:41 +01:00
misperrors = { ' error ' : ' Error ' }
2021-03-17 20:17:07 +01:00
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 ' ,
]
2020-11-03 19:30:09 +01:00
mispattributes = {
2021-03-17 20:17:07 +01:00
' input ' : standard_query_input + flex_query_input ,
2020-11-05 17:51:41 +01:00
' format ' : ' misp_standard '
2020-11-03 19:30:09 +01:00
}
moduleinfo = {
2021-03-17 20:17:07 +01:00
' version ' : ' 0.5 ' ,
2020-11-03 19:30:09 +01:00
' author ' : ' Christophe Vandeplas ' ,
2024-08-12 11:23:10 +02:00
' description ' : ' Module to access Farsight DNSDB Passive DNS. ' ,
' module-type ' : [ ' expansion ' , ' hover ' ] ,
' name ' : ' Farsight DNSDB Lookup ' ,
' logo ' : ' farsight.png ' ,
' requirements ' : [ ' An access to the Farsight Passive DNS API (apikey) ' ] ,
' features ' : ' This module takes a domain, hostname or IP address MISP attribute as input to query the Farsight Passive DNS API. \n The results of rdata and rrset lookups are then returned and parsed into passive-dns objects. \n \n An API key is required to submit queries to the API. \n It is also possible to define a custom server URL, and to set a limit of results to get. \n This limit is set for each lookup, which means we can have an up to the limit number of passive-dns objects resulting from an rdata query about an IP address, but an up to the limit number of passive-dns objects for each lookup queries about a domain or a hostname (== twice the limit). ' ,
' references ' : [ ' https://www.farsightsecurity.com/ ' , ' https://docs.dnsdb.info/dnsdb-api/ ' ] ,
' input ' : ' A domain, hostname or IP address MISP attribute. ' ,
' output ' : ' Passive-dns objects, resulting from the query on the Farsight Passive DNS API. ' ,
2020-11-03 19:30:09 +01:00
}
2020-11-13 20:38:02 +01:00
moduleconfig = [ ' apikey ' , ' server ' , ' limit ' , ' flex_queries ' ]
2020-11-03 19:30:09 +01:00
2020-11-12 16:01:14 +01:00
DEFAULT_DNSDB_SERVER = ' https://api.dnsdb.info '
2020-11-03 19:30:09 +01:00
DEFAULT_LIMIT = 10
2021-05-12 18:12:25 +02:00
DEFAULT_DISTRIBUTION_SETTING = Distribution . your_organisation_only . value
2020-11-15 20:11:08 +01:00
TYPE_TO_FEATURE = {
2021-03-17 20:17:07 +01:00
" btc " : " Bitcoin address " ,
" dkim " : " domainkeys identified mail " ,
2020-11-15 20:11:08 +01:00
" domain " : " domain name " ,
2021-03-17 20:17:07 +01:00
" domain|ip " : " domain name / IP address " ,
" hex " : " value in hexadecimal format " ,
2020-11-15 20:11:08 +01:00
" hostname " : " hostname " ,
2021-03-17 20:17:07 +01:00
" 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 "
2020-11-15 20:11:08 +01:00
}
2021-03-17 20:17:07 +01:00
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 "
)
)
2020-11-15 20:11:08 +01:00
2020-11-03 19:30:09 +01:00
class FarsightDnsdbParser ( ) :
def __init__ ( self , attribute ) :
self . attribute = attribute
self . misp_event = MISPEvent ( )
self . misp_event . add_attribute ( * * attribute )
self . passivedns_mapping = {
2021-05-03 11:25:37 +02:00
' bailiwick ' : { ' type ' : ' domain ' , ' object_relation ' : ' bailiwick ' } ,
2020-11-03 19:30:09 +01:00
' count ' : { ' type ' : ' counter ' , ' object_relation ' : ' count ' } ,
2020-11-13 20:38:02 +01:00
' raw_rdata ' : { ' type ' : ' text ' , ' object_relation ' : ' raw_rdata ' } ,
2020-11-03 19:30:09 +01:00
' 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 ' }
}
2021-03-17 20:17:07 +01:00
self . comment = ' Result from a %s lookup on DNSDB about the %s : %s '
2020-11-03 19:30:09 +01:00
def parse_passivedns_results ( self , query_response ) :
2020-11-04 18:37:57 +01:00
for query_type , results in query_response . items ( ) :
2020-11-15 20:11:08 +01:00
comment = self . comment % ( query_type , TYPE_TO_FEATURE [ self . attribute [ ' type ' ] ] , self . attribute [ ' value ' ] )
2020-11-04 18:37:57 +01:00
for result in results :
passivedns_object = MISPObject ( ' passive-dns ' )
2021-05-12 15:08:55 +02:00
passivedns_object . distribution = DEFAULT_DISTRIBUTION_SETTING
2020-11-13 20:38:02 +01:00
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 ) )
2021-03-31 13:42:07 +02:00
if result . get ( ' time_first ' ) :
passivedns_object . first_seen = result [ ' time_first ' ]
if result . get ( ' time_last ' ) :
passivedns_object . last_seen = result [ ' time_last ' ]
2020-11-04 18:37:57 +01:00
passivedns_object . add_reference ( self . attribute [ ' uuid ' ] , ' related-to ' )
self . misp_event . add_object ( passivedns_object )
2020-11-03 19:30:09 +01:00
def get_results ( self ) :
event = json . loads ( self . misp_event . to_json ( ) )
results = { key : event [ key ] for key in ( ' Attribute ' , ' Object ' ) }
return { ' results ' : results }
2020-11-04 18:37:57 +01:00
def _parse_attribute ( self , comment , feature , value ) :
2021-05-12 15:08:55 +02:00
attribute = { ' value ' : value , ' comment ' : comment , ' distribution ' : DEFAULT_DISTRIBUTION_SETTING }
2020-11-03 19:30:09 +01:00
attribute . update ( self . passivedns_mapping [ feature ] )
return attribute
2017-12-05 16:41:41 +01:00
def handler ( q = False ) :
if q is False :
return False
request = json . loads ( q )
2019-10-30 09:09:55 +01:00
if not request . get ( ' config ' ) or not request [ ' config ' ] . get ( ' apikey ' ) :
misperrors [ ' error ' ] = ' Farsight DNSDB apikey is missing '
return misperrors
2020-11-03 19:30:09 +01:00
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 ' ]
2020-11-13 20:38:02 +01:00
if not config . get ( ' server ' ) :
2020-11-12 16:01:14 +01:00
config [ ' server ' ] = DEFAULT_DNSDB_SERVER
client_args = { feature : config [ feature ] for feature in ( ' apikey ' , ' server ' ) }
client = dnsdb2 . Client ( * * client_args )
2021-03-17 20:17:07 +01:00
to_query , args = parse_input ( attribute , config )
2020-11-13 20:38:02 +01:00
try :
2021-03-17 20:17:07 +01:00
response = to_query ( client , * args )
2020-11-13 20:38:02 +01:00
except dnsdb2 . DnsdbException as e :
return { ' error ' : e . __str__ ( ) }
2021-05-04 18:36:56 +02:00
except dnsdb2 . exceptions . QueryError :
return { ' error ' : ' Communication error occurs while executing a query, or the server reports an error due to invalid arguments. ' }
2020-11-03 19:30:09 +01:00
if not response :
2020-11-15 20:11:08 +01:00
return { ' error ' : f " Empty results on Farsight DNSDB for the { TYPE_TO_FEATURE [ attribute [ ' type ' ] ] } : { attribute [ ' value ' ] } . " }
2020-11-03 19:30:09 +01:00
parser = FarsightDnsdbParser ( attribute )
parser . parse_passivedns_results ( response )
return parser . get_results ( )
2017-12-05 16:41:41 +01:00
2021-03-17 20:17:07 +01:00
def parse_input ( attribute , config ) :
lookup_args = {
' limit ' : config [ ' limit ' ] if config . get ( ' limit ' ) else DEFAULT_LIMIT ,
' offset ' : 0 ,
2021-03-30 03:47:34 +02:00
' ignore_limited ' : True ,
' humantime ' : True
2021-03-17 20:17:07 +01:00
}
2021-03-29 20:09:29 +02:00
if attribute . get ( ' first_seen ' ) :
lookup_args [ ' time_first_after ' ] = parse_timestamp ( attribute [ ' first_seen ' ] )
2021-03-17 20:17:07 +01:00
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 )
2021-04-14 14:45:55 +02:00
2021-03-29 20:09:29 +02:00
def parse_timestamp ( str_date ) :
datetime_date = datetime . strptime ( str_date , ' % Y- % m- %d T % H: % M: % S. %f % z ' )
return str ( int ( datetime_date . timestamp ( ) ) )
2021-03-17 20:17:07 +01:00
2021-04-14 14:45:55 +02:00
2020-11-13 20:38:02 +01:00
def add_flex_queries ( flex ) :
if not flex :
return False
if flex in ( ' True ' , ' true ' , True , ' 1 ' , 1 ) :
return True
return False
2021-03-17 20:17:07 +01:00
def flex_queries ( client , lookup_args , name ) :
2020-11-04 18:37:57 +01:00
response = { }
2021-03-18 18:40:27 +01:00
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
2020-11-03 19:30:09 +01:00
return response
2017-12-05 16:41:41 +01:00
2021-03-17 20:17:07 +01:00
def lookup_name ( client , lookup_args , name , flex ) :
2020-11-13 20:38:02 +01:00
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
2021-03-30 03:42:54 +02:00
rdata_response = list ( client . lookup_rdata_name ( name , * * lookup_args ) )
2020-11-13 20:38:02 +01:00
if rdata_response :
response [ ' rdata ' ] = rdata_response
if flex :
2021-03-17 20:17:07 +01:00
response . update ( flex_queries ( client , lookup_args , name ) )
2020-11-13 20:38:02 +01:00
return response
2021-03-17 20:17:07 +01:00
def lookup_ip ( client , lookup_args , ip , flex ) :
2020-11-13 20:38:02 +01:00
response = { }
res = list ( client . lookup_rdata_ip ( ip , * * lookup_args ) )
if res :
response [ ' rdata ' ] = res
if flex :
2021-03-17 20:17:07 +01:00
response . update ( flex_queries ( client , lookup_args , ip ) )
2020-11-03 19:30:09 +01:00
return response
2017-12-05 16:41:41 +01:00
def introspection ( ) :
return mispattributes
def version ( ) :
moduleinfo [ ' config ' ] = moduleconfig
return moduleinfo