Merge branch 'main' of github.com:MISP/misp-modules into main

pull/445/head
chrisr3d 2020-11-15 19:27:54 +01:00
commit eac43b0dc7
7 changed files with 138 additions and 63 deletions

View File

@ -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)

View File

@ -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)."
}

View File

@ -119,7 +119,10 @@ class DnsdbClient(object):
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):

View File

@ -6,19 +6,19 @@ from pymisp import MISPEvent, MISPObject
misperrors = {'error': 'Error'}
mispattributes = {'input': ['cpe'], 'format': 'misp_standard'}
moduleinfo = {
'version': '1',
'version': '2',
'author': 'Christian Studer',
'description': 'An expansion module to enrich a CPE attribute with its related vulnerabilities.',
'module-type': ['expansion', 'hover']
}
moduleconfig = ["custom_API_URL", "limit"]
cveapi_url = 'https://cvepremium.circl.lu/api/cvefor/'
cveapi_url = 'https://cvepremium.circl.lu/api/query'
DEFAULT_LIMIT = 10
class VulnerabilitiesParser():
def __init__(self, attribute, api_url):
def __init__(self, attribute):
self.attribute = attribute
self.api_url = api_url
self.misp_event = MISPEvent()
self.misp_event.add_attribute(**attribute)
self.vulnerability_mapping = {
@ -32,11 +32,11 @@ class VulnerabilitiesParser():
},
'vulnerable_configuration': {
'type': 'cpe',
'object_relation': 'vulnerable_configuration'
'object_relation': 'vulnerable-configuration'
},
'vulnerable_configuration_cpe_2_2': {
'type': 'cpe',
'object_relation': 'vulnerable_configuration'
'object_relation': 'vulnerable-configuration'
},
'Modified': {
'type': 'datetime',
@ -100,18 +100,26 @@ def handler(q=False):
attribute = request['attribute']
if attribute.get('type') != 'cpe':
return {'error': 'Wrong input attribute type.'}
api_url = check_url(request['config']['custom_API_URL']) if request['config'].get('custom_API_URL') else cveapi_url
url = f"{api_url}{attribute['value']}"
if request['config'].get('limit'):
url = f"{url}/{request['config']['limit']}"
response = requests.get(url)
config = request['config']
url = check_url(config['custom_API_URL']) if config.get('custom_API_URL') else cveapi_url
limit = int(config['limit']) if config.get('limit') else DEFAULT_LIMIT
params = {
"retrieve": "cves",
"dict_filter": {
"vulnerable_configuration": attribute['value']
},
"limit": limit,
"sort": "cvss",
"sort_dir": "DESC"
}
response = requests.post(url, json=params)
if response.status_code == 200:
vulnerabilities = response.json()
vulnerabilities = response.json()['data']
if not vulnerabilities:
return {'error': 'No related vulnerability for this CPE.'}
else:
return {'error': 'API not accessible.'}
parser = VulnerabilitiesParser(attribute, api_url)
parser = VulnerabilitiesParser(attribute)
parser.parse_vulnerabilities(vulnerabilities)
return parser.get_result()

View File

@ -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']
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']
server = 'https://api.dnsdb.info'
DEFAULT_LIMIT = 10
# TODO return a MISP object with the different attributes
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):
@ -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():

View File

@ -39,7 +39,7 @@ class TruSTARParser:
# Relevant fields from each TruSTAR endpoint
SUMMARY_FIELDS = ["severityLevel", "source", "score", "attributes"]
METADATA_FIELDS = ["sightings", "first_seen", "last_seen", "tags"]
METADATA_FIELDS = ["sightings", "firstSeen", "lastSeen", "tags"]
REPORT_BASE_URL = "https://station.trustar.co/constellation/reports/{}"

View File

@ -221,7 +221,7 @@ class TestExpansions(unittest.TestCase):
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)
@ -285,7 +285,7 @@ class TestExpansions(unittest.TestCase):
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'