Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/MISP/misp-modules into sour…
Browse files Browse the repository at this point in the history
…ce_confidence
  • Loading branch information
redsand committed Nov 11, 2020
2 parents 7e77058 + ab23547 commit 79acdec
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 51 deletions.
11 changes: 8 additions & 3 deletions doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,13 +505,18 @@ A module to query the Phishing Initiative service (https://phishing-initiative.l

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.
>This module takes a domain, hostname or IP address MISP attribute as input to query the Farsight Passive DNS API.
> The results of rdata and rrset lookups are then returned and parsed into passive-dns objects.
>
>An API key is required to submit queries to the API.
> It is also possible to define a custom server URL, and to set a limit of results to get.
> 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).
- **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.
>Passive-dns objects, resulting from the query on the Farsight Passive DNS API.
- **references**:
>https://www.farsightsecurity.com/
>https://www.farsightsecurity.com/, https://docs.dnsdb.info/dnsdb-api/
- **requirements**:
>An access to the Farsight Passive DNS API (apikey)
Expand Down
6 changes: 3 additions & 3 deletions doc/expansion/farsight_passivedns.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"logo": "logos/farsight.png",
"requirements": ["An access to the Farsight Passive DNS API (apikey)"],
"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/"],
"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."
"output": "Passive-dns objects, resulting from the query on the Farsight Passive DNS API.",
"references": ["https://www.farsightsecurity.com/", "https://docs.dnsdb.info/dnsdb-api/"],
"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\nAn 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)."
}
5 changes: 4 additions & 1 deletion misp_modules/modules/expansion/_dnsdb_query/dnsdb_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ def _query(self, path, before=None, after=None):
break
yield json.loads(line.decode('ascii'))
except (HTTPError, URLError) as e:
raise QueryError(str(e), sys.exc_traceback)
try:
raise QueryError(str(e), sys.exc_traceback)
except AttributeError:
raise QueryError(str(e), sys.exc_info)


def quote(path):
Expand Down
143 changes: 101 additions & 42 deletions misp_modules/modules/expansion/farsight_passivedns.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,83 @@
import json
from ._dnsdb_query.dnsdb_query import DnsdbClient, QueryError

from ._dnsdb_query.dnsdb_query import DEFAULT_DNSDB_SERVER, DnsdbClient, QueryError
from . import check_input_attribute, standard_error_message
from pymisp import MISPEvent, MISPObject

misperrors = {'error': 'Error'}
mispattributes = {'input': ['hostname', 'domain', 'ip-src', 'ip-dst'], 'output': ['freetext']}
moduleinfo = {'version': '0.1', 'author': 'Christophe Vandeplas', 'description': 'Module to access Farsight DNSDB Passive DNS', 'module-type': ['expansion', 'hover']}
moduleconfig = ['apikey']

server = 'https://api.dnsdb.info'

# TODO return a MISP object with the different attributes
mispattributes = {
'input': ['hostname', 'domain', 'ip-src', 'ip-dst'],
'format': 'misp_standard'
}
moduleinfo = {
'version': '0.2',
'author': 'Christophe Vandeplas',
'description': 'Module to access Farsight DNSDB Passive DNS',
'module-type': ['expansion', 'hover']
}
moduleconfig = ['apikey', 'server', 'limit']

DEFAULT_LIMIT = 10


class FarsightDnsdbParser():
def __init__(self, attribute):
self.attribute = attribute
self.misp_event = MISPEvent()
self.misp_event.add_attribute(**attribute)
self.passivedns_mapping = {
'bailiwick': {'type': 'text', 'object_relation': 'bailiwick'},
'count': {'type': 'counter', 'object_relation': 'count'},
'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.type_to_feature = {
'domain': 'domain name',
'hostname': 'hostname',
'ip-src': 'IP address',
'ip-dst': 'IP address'
}
self.comment = 'Result from an %s lookup on DNSDB about the %s: %s'

def parse_passivedns_results(self, query_response):
default_fields = ('count', 'rrname', 'rrname')
optional_fields = (
'bailiwick',
'time_first',
'time_last',
'zone_time_first',
'zone_time_last'
)
for query_type, results in query_response.items():
comment = self.comment % (query_type, self.type_to_feature[self.attribute['type']], self.attribute['value'])
for result in results:
passivedns_object = MISPObject('passive-dns')
for feature in default_fields:
passivedns_object.add_attribute(**self._parse_attribute(comment, feature, result[feature]))
for feature in optional_fields:
if result.get(feature):
passivedns_object.add_attribute(**self._parse_attribute(comment, feature, result[feature]))
if isinstance(result['rdata'], list):
for rdata in result['rdata']:
passivedns_object.add_attribute(**self._parse_attribute(comment, 'rdata', rdata))
else:
passivedns_object.add_attribute(**self._parse_attribute(comment, 'rdata', result['rdata']))
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}
attribute.update(self.passivedns_mapping[feature])
return attribute


def handler(q=False):
Expand All @@ -19,56 +87,47 @@ def handler(q=False):
if not request.get('config') or not request['config'].get('apikey'):
misperrors['error'] = 'Farsight DNSDB apikey is missing'
return misperrors
client = DnsdbClient(server, request['config']['apikey'])
if request.get('hostname'):
res = lookup_name(client, request['hostname'])
elif request.get('domain'):
res = lookup_name(client, request['domain'])
elif request.get('ip-src'):
res = lookup_ip(client, request['ip-src'])
elif request.get('ip-dst'):
res = lookup_ip(client, request['ip-dst'])
else:
misperrors['error'] = "Unsupported attributes type"
return misperrors

out = ''
for v in set(res): # uniquify entries
out = out + "{} ".format(v)
r = {'results': [{'types': mispattributes['output'], 'values': out}]}
return r
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']
args = {'apikey': config['apikey']}
for feature, default in zip(('server', 'limit'), (DEFAULT_DNSDB_SERVER, DEFAULT_LIMIT)):
args[feature] = config[feature] if config.get(feature) else default
client = DnsdbClient(**args)
to_query = lookup_ip if attribute['type'] in ('ip-src', 'ip-dst') else lookup_name
response = to_query(client, attribute['value'])
if not response:
return {'error': f"Empty results on Farsight DNSDB for the queries {attribute['type']}: {attribute['value']}."}
parser = FarsightDnsdbParser(attribute)
parser.parse_passivedns_results(response)
return parser.get_results()


def lookup_name(client, name):
response = {}
try:
res = client.query_rrset(name) # RRSET = entries in the left-hand side of the domain name related labels
for item in res:
if item.get('rrtype') in ['A', 'AAAA', 'CNAME']:
for i in item.get('rdata'):
yield(i.rstrip('.'))
if item.get('rrtype') in ['SOA']:
for i in item.get('rdata'):
# grab email field and replace first dot by @ to convert to an email address
yield(i.split(' ')[1].rstrip('.').replace('.', '@', 1))
response['rrset'] = list(res)
except QueryError:
pass

try:
res = client.query_rdata_name(name) # RDATA = entries on the right-hand side of the domain name related labels
for item in res:
if item.get('rrtype') in ['A', 'AAAA', 'CNAME']:
yield(item.get('rrname').rstrip('.'))
response['rdata'] = list(res)
except QueryError:
pass
return response


def lookup_ip(client, ip):
try:
res = client.query_rdata_ip(ip)
for item in res:
yield(item['rrname'].rstrip('.'))
response = {'rdata': list(res)}
except QueryError:
pass
response = {}
return response


def introspection():
Expand Down
4 changes: 2 additions & 2 deletions tests/test_expansions.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def test_farsight_passivedns(self):
try:
self.assertIn(result, self.get_values(response))
except Exception:
self.assertTrue(self.get_errors(response).startwith('Something went wrong'))
self.assertTrue(self.get_errors(response).startswith('Something went wrong'))
else:
query = {"module": module_name, "ip-src": "8.8.8.8"}
response = self.misp_modules_post(query)
Expand Down Expand Up @@ -285,7 +285,7 @@ def test_ocr(self):
encoded = b64encode(f.read()).decode()
query = {"module": "ocr_enrich", "attachment": filename, "data": encoded}
response = self.misp_modules_post(query)
self.assertEqual(self.get_values(response), 'Threat Sharing')
self.assertEqual(self.get_values(response).strip('\n'), 'Threat Sharing')

def test_ods(self):
filename = 'test.ods'
Expand Down

0 comments on commit 79acdec

Please sign in to comment.