Skip to content

Commit 79acdec

Browse files
committed
Merge branch 'main' of https://github.com/MISP/misp-modules into source_confidence
2 parents 7e77058 + ab23547 commit 79acdec

File tree

5 files changed

+118
-51
lines changed

5 files changed

+118
-51
lines changed

doc/README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -505,13 +505,18 @@ A module to query the Phishing Initiative service (https://phishing-initiative.l
505505

506506
Module to access Farsight DNSDB Passive DNS.
507507
- **features**:
508-
>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.
508+
>This module takes a domain, hostname or IP address MISP attribute as input to query the Farsight Passive DNS API.
509+
> The results of rdata and rrset lookups are then returned and parsed into passive-dns objects.
510+
>
511+
>An API key is required to submit queries to the API.
512+
> It is also possible to define a custom server URL, and to set a limit of results to get.
513+
> 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).
509514
- **input**:
510515
>A domain, hostname or IP address MISP attribute.
511516
- **output**:
512-
>Text containing information about the input, resulting from the query on the Farsight Passive DNS API.
517+
>Passive-dns objects, resulting from the query on the Farsight Passive DNS API.
513518
- **references**:
514-
>https://www.farsightsecurity.com/
519+
>https://www.farsightsecurity.com/, https://docs.dnsdb.info/dnsdb-api/
515520
- **requirements**:
516521
>An access to the Farsight Passive DNS API (apikey)
517522

doc/expansion/farsight_passivedns.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"logo": "logos/farsight.png",
44
"requirements": ["An access to the Farsight Passive DNS API (apikey)"],
55
"input": "A domain, hostname or IP address MISP attribute.",
6-
"output": "Text containing information about the input, resulting from the query on the Farsight Passive DNS API.",
7-
"references": ["https://www.farsightsecurity.com/"],
8-
"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."
6+
"output": "Passive-dns objects, resulting from the query on the Farsight Passive DNS API.",
7+
"references": ["https://www.farsightsecurity.com/", "https://docs.dnsdb.info/dnsdb-api/"],
8+
"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)."
99
}

misp_modules/modules/expansion/_dnsdb_query/dnsdb_query.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,10 @@ def _query(self, path, before=None, after=None):
119119
break
120120
yield json.loads(line.decode('ascii'))
121121
except (HTTPError, URLError) as e:
122-
raise QueryError(str(e), sys.exc_traceback)
122+
try:
123+
raise QueryError(str(e), sys.exc_traceback)
124+
except AttributeError:
125+
raise QueryError(str(e), sys.exc_info)
123126

124127

125128
def quote(path):

misp_modules/modules/expansion/farsight_passivedns.py

Lines changed: 101 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,83 @@
11
import json
2-
from ._dnsdb_query.dnsdb_query import DnsdbClient, QueryError
3-
2+
from ._dnsdb_query.dnsdb_query import DEFAULT_DNSDB_SERVER, DnsdbClient, QueryError
3+
from . import check_input_attribute, standard_error_message
4+
from pymisp import MISPEvent, MISPObject
45

56
misperrors = {'error': 'Error'}
6-
mispattributes = {'input': ['hostname', 'domain', 'ip-src', 'ip-dst'], 'output': ['freetext']}
7-
moduleinfo = {'version': '0.1', 'author': 'Christophe Vandeplas', 'description': 'Module to access Farsight DNSDB Passive DNS', 'module-type': ['expansion', 'hover']}
8-
moduleconfig = ['apikey']
9-
10-
server = 'https://api.dnsdb.info'
11-
12-
# TODO return a MISP object with the different attributes
7+
mispattributes = {
8+
'input': ['hostname', 'domain', 'ip-src', 'ip-dst'],
9+
'format': 'misp_standard'
10+
}
11+
moduleinfo = {
12+
'version': '0.2',
13+
'author': 'Christophe Vandeplas',
14+
'description': 'Module to access Farsight DNSDB Passive DNS',
15+
'module-type': ['expansion', 'hover']
16+
}
17+
moduleconfig = ['apikey', 'server', 'limit']
18+
19+
DEFAULT_LIMIT = 10
20+
21+
22+
class FarsightDnsdbParser():
23+
def __init__(self, attribute):
24+
self.attribute = attribute
25+
self.misp_event = MISPEvent()
26+
self.misp_event.add_attribute(**attribute)
27+
self.passivedns_mapping = {
28+
'bailiwick': {'type': 'text', 'object_relation': 'bailiwick'},
29+
'count': {'type': 'counter', 'object_relation': 'count'},
30+
'rdata': {'type': 'text', 'object_relation': 'rdata'},
31+
'rrname': {'type': 'text', 'object_relation': 'rrname'},
32+
'rrtype': {'type': 'text', 'object_relation': 'rrtype'},
33+
'time_first': {'type': 'datetime', 'object_relation': 'time_first'},
34+
'time_last': {'type': 'datetime', 'object_relation': 'time_last'},
35+
'zone_time_first': {'type': 'datetime', 'object_relation': 'zone_time_first'},
36+
'zone_time_last': {'type': 'datetime', 'object_relation': 'zone_time_last'}
37+
}
38+
self.type_to_feature = {
39+
'domain': 'domain name',
40+
'hostname': 'hostname',
41+
'ip-src': 'IP address',
42+
'ip-dst': 'IP address'
43+
}
44+
self.comment = 'Result from an %s lookup on DNSDB about the %s: %s'
45+
46+
def parse_passivedns_results(self, query_response):
47+
default_fields = ('count', 'rrname', 'rrname')
48+
optional_fields = (
49+
'bailiwick',
50+
'time_first',
51+
'time_last',
52+
'zone_time_first',
53+
'zone_time_last'
54+
)
55+
for query_type, results in query_response.items():
56+
comment = self.comment % (query_type, self.type_to_feature[self.attribute['type']], self.attribute['value'])
57+
for result in results:
58+
passivedns_object = MISPObject('passive-dns')
59+
for feature in default_fields:
60+
passivedns_object.add_attribute(**self._parse_attribute(comment, feature, result[feature]))
61+
for feature in optional_fields:
62+
if result.get(feature):
63+
passivedns_object.add_attribute(**self._parse_attribute(comment, feature, result[feature]))
64+
if isinstance(result['rdata'], list):
65+
for rdata in result['rdata']:
66+
passivedns_object.add_attribute(**self._parse_attribute(comment, 'rdata', rdata))
67+
else:
68+
passivedns_object.add_attribute(**self._parse_attribute(comment, 'rdata', result['rdata']))
69+
passivedns_object.add_reference(self.attribute['uuid'], 'related-to')
70+
self.misp_event.add_object(passivedns_object)
71+
72+
def get_results(self):
73+
event = json.loads(self.misp_event.to_json())
74+
results = {key: event[key] for key in ('Attribute', 'Object')}
75+
return {'results': results}
76+
77+
def _parse_attribute(self, comment, feature, value):
78+
attribute = {'value': value, 'comment': comment}
79+
attribute.update(self.passivedns_mapping[feature])
80+
return attribute
1381

1482

1583
def handler(q=False):
@@ -19,56 +87,47 @@ def handler(q=False):
1987
if not request.get('config') or not request['config'].get('apikey'):
2088
misperrors['error'] = 'Farsight DNSDB apikey is missing'
2189
return misperrors
22-
client = DnsdbClient(server, request['config']['apikey'])
23-
if request.get('hostname'):
24-
res = lookup_name(client, request['hostname'])
25-
elif request.get('domain'):
26-
res = lookup_name(client, request['domain'])
27-
elif request.get('ip-src'):
28-
res = lookup_ip(client, request['ip-src'])
29-
elif request.get('ip-dst'):
30-
res = lookup_ip(client, request['ip-dst'])
31-
else:
32-
misperrors['error'] = "Unsupported attributes type"
33-
return misperrors
34-
35-
out = ''
36-
for v in set(res): # uniquify entries
37-
out = out + "{} ".format(v)
38-
r = {'results': [{'types': mispattributes['output'], 'values': out}]}
39-
return r
90+
if not request.get('attribute') or not check_input_attribute(request['attribute']):
91+
return {'error': f'{standard_error_message}, which should contain at least a type, a value and an uuid.'}
92+
attribute = request['attribute']
93+
if attribute['type'] not in mispattributes['input']:
94+
return {'error': 'Unsupported attributes type'}
95+
config = request['config']
96+
args = {'apikey': config['apikey']}
97+
for feature, default in zip(('server', 'limit'), (DEFAULT_DNSDB_SERVER, DEFAULT_LIMIT)):
98+
args[feature] = config[feature] if config.get(feature) else default
99+
client = DnsdbClient(**args)
100+
to_query = lookup_ip if attribute['type'] in ('ip-src', 'ip-dst') else lookup_name
101+
response = to_query(client, attribute['value'])
102+
if not response:
103+
return {'error': f"Empty results on Farsight DNSDB for the queries {attribute['type']}: {attribute['value']}."}
104+
parser = FarsightDnsdbParser(attribute)
105+
parser.parse_passivedns_results(response)
106+
return parser.get_results()
40107

41108

42109
def lookup_name(client, name):
110+
response = {}
43111
try:
44112
res = client.query_rrset(name) # RRSET = entries in the left-hand side of the domain name related labels
45-
for item in res:
46-
if item.get('rrtype') in ['A', 'AAAA', 'CNAME']:
47-
for i in item.get('rdata'):
48-
yield(i.rstrip('.'))
49-
if item.get('rrtype') in ['SOA']:
50-
for i in item.get('rdata'):
51-
# grab email field and replace first dot by @ to convert to an email address
52-
yield(i.split(' ')[1].rstrip('.').replace('.', '@', 1))
113+
response['rrset'] = list(res)
53114
except QueryError:
54115
pass
55-
56116
try:
57117
res = client.query_rdata_name(name) # RDATA = entries on the right-hand side of the domain name related labels
58-
for item in res:
59-
if item.get('rrtype') in ['A', 'AAAA', 'CNAME']:
60-
yield(item.get('rrname').rstrip('.'))
118+
response['rdata'] = list(res)
61119
except QueryError:
62120
pass
121+
return response
63122

64123

65124
def lookup_ip(client, ip):
66125
try:
67126
res = client.query_rdata_ip(ip)
68-
for item in res:
69-
yield(item['rrname'].rstrip('.'))
127+
response = {'rdata': list(res)}
70128
except QueryError:
71-
pass
129+
response = {}
130+
return response
72131

73132

74133
def introspection():

tests/test_expansions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ def test_farsight_passivedns(self):
221221
try:
222222
self.assertIn(result, self.get_values(response))
223223
except Exception:
224-
self.assertTrue(self.get_errors(response).startwith('Something went wrong'))
224+
self.assertTrue(self.get_errors(response).startswith('Something went wrong'))
225225
else:
226226
query = {"module": module_name, "ip-src": "8.8.8.8"}
227227
response = self.misp_modules_post(query)
@@ -285,7 +285,7 @@ def test_ocr(self):
285285
encoded = b64encode(f.read()).decode()
286286
query = {"module": "ocr_enrich", "attachment": filename, "data": encoded}
287287
response = self.misp_modules_post(query)
288-
self.assertEqual(self.get_values(response), 'Threat Sharing')
288+
self.assertEqual(self.get_values(response).strip('\n'), 'Threat Sharing')
289289

290290
def test_ods(self):
291291
filename = 'test.ods'

0 commit comments

Comments
 (0)