mirror of https://github.com/MISP/misp-modules
264 lines
6.9 KiB
Python
264 lines
6.9 KiB
Python
|
# Copyright (c) 2009-2021 Qintel, LLC
|
||
|
# Licensed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0.txt)
|
||
|
|
||
|
from urllib.request import Request, urlopen
|
||
|
from urllib.parse import urlencode
|
||
|
from urllib.error import HTTPError
|
||
|
from time import sleep
|
||
|
from json import loads
|
||
|
import os
|
||
|
from copy import deepcopy
|
||
|
from datetime import datetime, timedelta
|
||
|
from gzip import GzipFile
|
||
|
|
||
|
VERSION = '1.0.1'
|
||
|
USER_AGENT = 'integrations-helper'
|
||
|
MAX_RETRY_ATTEMPTS = 5
|
||
|
|
||
|
DEFAULT_HEADERS = {
|
||
|
'User-Agent': f'{USER_AGENT}/{VERSION}'
|
||
|
}
|
||
|
|
||
|
REMOTE_MAP = {
|
||
|
'pmi': 'https://api.pmi.qintel.com',
|
||
|
'qwatch': 'https://api.qwatch.qintel.com',
|
||
|
'qauth': 'https://api.qauth.qintel.com',
|
||
|
'qsentry_feed': 'https://qsentry.qintel.com',
|
||
|
'qsentry': 'https://api.qsentry.qintel.com'
|
||
|
}
|
||
|
|
||
|
ENDPOINT_MAP = {
|
||
|
'pmi': {
|
||
|
'ping': '/users/me',
|
||
|
'cve': 'cves'
|
||
|
},
|
||
|
'qsentry_feed': {
|
||
|
'anon': '/files/anonymization',
|
||
|
'mal_hosting': '/files/malicious_hosting'
|
||
|
},
|
||
|
'qsentry': {},
|
||
|
'qwatch': {
|
||
|
'ping': '/users/me',
|
||
|
'exposures': 'exposures'
|
||
|
},
|
||
|
'qauth': {}
|
||
|
}
|
||
|
|
||
|
|
||
|
def _get_request_wait_time(attempts):
|
||
|
""" Use Fibonacci numbers for determining the time to wait when rate limits
|
||
|
have been encountered.
|
||
|
"""
|
||
|
|
||
|
n = attempts + 3
|
||
|
a, b = 1, 0
|
||
|
for _ in range(n):
|
||
|
a, b = a + b, a
|
||
|
|
||
|
return a
|
||
|
|
||
|
|
||
|
def _search(**kwargs):
|
||
|
remote = kwargs.get('remote')
|
||
|
max_retries = int(kwargs.get('max_retries', MAX_RETRY_ATTEMPTS))
|
||
|
params = kwargs.get('params', {})
|
||
|
headers = _set_headers(**kwargs)
|
||
|
|
||
|
logger = kwargs.get('logger')
|
||
|
|
||
|
params = urlencode(params)
|
||
|
url = remote + "?" + params
|
||
|
req = Request(url, headers=headers)
|
||
|
|
||
|
request_attempts = 1
|
||
|
while request_attempts < max_retries:
|
||
|
try:
|
||
|
return urlopen(req)
|
||
|
|
||
|
except HTTPError as e:
|
||
|
response = e
|
||
|
|
||
|
except Exception as e:
|
||
|
raise Exception('API connection error') from e
|
||
|
|
||
|
if response.code not in [429, 504]:
|
||
|
raise Exception(f'API connection error: {response}')
|
||
|
|
||
|
if request_attempts < max_retries:
|
||
|
wait_time = _get_request_wait_time(request_attempts)
|
||
|
|
||
|
if response.code == 429:
|
||
|
msg = 'rate limit reached on attempt {request_attempts}, ' \
|
||
|
'waiting {wait_time} seconds'
|
||
|
|
||
|
if logger:
|
||
|
logger(msg)
|
||
|
|
||
|
else:
|
||
|
msg = f'connection timed out, retrying in {wait_time} seconds'
|
||
|
if logger:
|
||
|
logger(msg)
|
||
|
|
||
|
sleep(wait_time)
|
||
|
|
||
|
else:
|
||
|
raise Exception('Max API retries exceeded')
|
||
|
|
||
|
request_attempts += 1
|
||
|
|
||
|
|
||
|
def _set_headers(**kwargs):
|
||
|
headers = deepcopy(DEFAULT_HEADERS)
|
||
|
|
||
|
if kwargs.get('user_agent'):
|
||
|
headers['User-Agent'] = \
|
||
|
f"{kwargs['user_agent']}/{USER_AGENT}/{VERSION}"
|
||
|
|
||
|
# TODO: deprecate
|
||
|
if kwargs.get('client_id') or kwargs.get('client_secret'):
|
||
|
try:
|
||
|
headers['Cf-Access-Client-Id'] = kwargs['client_id']
|
||
|
headers['Cf-Access-Client-Secret'] = kwargs['client_secret']
|
||
|
except KeyError:
|
||
|
raise Exception('missing client_id or client_secret')
|
||
|
|
||
|
if kwargs.get('token'):
|
||
|
headers['x-api-key'] = kwargs['token']
|
||
|
|
||
|
return headers
|
||
|
|
||
|
|
||
|
def _set_remote(product, query_type, **kwargs):
|
||
|
remote = kwargs.get('remote')
|
||
|
endpoint = kwargs.get('endpoint', ENDPOINT_MAP[product].get(query_type))
|
||
|
|
||
|
if not remote:
|
||
|
remote = REMOTE_MAP[product]
|
||
|
|
||
|
if not endpoint:
|
||
|
raise Exception('invalid search type')
|
||
|
|
||
|
remote = remote.rstrip('/')
|
||
|
endpoint = endpoint.lstrip('/')
|
||
|
|
||
|
return f'{remote}/{endpoint}'
|
||
|
|
||
|
|
||
|
def _process_qsentry(resp):
|
||
|
if resp.getheader('Content-Encoding', '') == 'gzip':
|
||
|
with GzipFile(fileobj=resp) as file:
|
||
|
for line in file.readlines():
|
||
|
yield loads(line)
|
||
|
|
||
|
|
||
|
def search_pmi(search_term, query_type, **kwargs):
|
||
|
"""
|
||
|
Search PMI
|
||
|
|
||
|
:param str search_term: Search term
|
||
|
:param str query_type: Query type [cve|ping]
|
||
|
:param dict kwargs: extra client args [remote|token|params]
|
||
|
:return: API JSON response object
|
||
|
:rtype: dict
|
||
|
"""
|
||
|
|
||
|
kwargs['remote'] = _set_remote('pmi', query_type, **kwargs)
|
||
|
kwargs['token'] = kwargs.get('token', os.getenv('PMI_TOKEN'))
|
||
|
|
||
|
params = kwargs.get('params', {})
|
||
|
params.update({'identifier': search_term})
|
||
|
kwargs['params'] = params
|
||
|
|
||
|
return loads(_search(**kwargs).read())
|
||
|
|
||
|
|
||
|
def search_qwatch(search_term, search_type, query_type, **kwargs):
|
||
|
"""
|
||
|
Search QWatch for exposed credentials
|
||
|
|
||
|
:param str search_term: Search term
|
||
|
:param str search_type: Search term type [domain|email]
|
||
|
:param str query_type: Query type [exposures]
|
||
|
:param dict kwargs: extra client args [remote|token|params]
|
||
|
:return: API JSON response object
|
||
|
:rtype: dict
|
||
|
"""
|
||
|
|
||
|
kwargs['remote'] = _set_remote('qwatch', query_type, **kwargs)
|
||
|
kwargs['token'] = kwargs.get('token', os.getenv('QWATCH_TOKEN'))
|
||
|
|
||
|
params = kwargs.get('params', {})
|
||
|
if search_type:
|
||
|
params.update({search_type: search_term})
|
||
|
kwargs['params'] = params
|
||
|
|
||
|
return loads(_search(**kwargs).read())
|
||
|
|
||
|
|
||
|
def search_qauth(search_term, **kwargs):
|
||
|
"""
|
||
|
Search QAuth
|
||
|
|
||
|
:param str search_term: Search term
|
||
|
:param dict kwargs: extra client args [remote|token|params]
|
||
|
:return: API JSON response object
|
||
|
:rtype: dict
|
||
|
"""
|
||
|
|
||
|
if not kwargs.get('endpoint'):
|
||
|
kwargs['endpoint'] = '/'
|
||
|
|
||
|
kwargs['remote'] = _set_remote('qauth', None, **kwargs)
|
||
|
kwargs['token'] = kwargs.get('token', os.getenv('QAUTH_TOKEN'))
|
||
|
|
||
|
params = kwargs.get('params', {})
|
||
|
params.update({'q': search_term})
|
||
|
kwargs['params'] = params
|
||
|
|
||
|
return loads(_search(**kwargs).read())
|
||
|
|
||
|
|
||
|
def search_qsentry(search_term, **kwargs):
|
||
|
"""
|
||
|
Search QSentry
|
||
|
|
||
|
:param str search_term: Search term
|
||
|
:param dict kwargs: extra client args [remote|token|params]
|
||
|
:return: API JSON response object
|
||
|
:rtype: dict
|
||
|
"""
|
||
|
|
||
|
if not kwargs.get('endpoint'):
|
||
|
kwargs['endpoint'] = '/'
|
||
|
|
||
|
kwargs['remote'] = _set_remote('qsentry', None, **kwargs)
|
||
|
kwargs['token'] = kwargs.get('token', os.getenv('QSENTRY_TOKEN'))
|
||
|
|
||
|
params = kwargs.get('params', {})
|
||
|
params.update({'q': search_term})
|
||
|
kwargs['params'] = params
|
||
|
|
||
|
return loads(_search(**kwargs).read())
|
||
|
|
||
|
|
||
|
def qsentry_feed(query_type='anon', feed_date=datetime.today(), **kwargs):
|
||
|
"""
|
||
|
Fetch the most recent QSentry Feed
|
||
|
|
||
|
:param str query_type: Feed type [anon|mal_hosting]
|
||
|
:param dict kwargs: extra client args [remote|token|params]
|
||
|
:param datetime feed_date: feed date to fetch
|
||
|
:return: API JSON response object
|
||
|
:rtype: Iterator[dict]
|
||
|
"""
|
||
|
|
||
|
remote = _set_remote('qsentry_feed', query_type, **kwargs)
|
||
|
kwargs['token'] = kwargs.get('token', os.getenv('QSENTRY_TOKEN'))
|
||
|
|
||
|
feed_date = (feed_date - timedelta(days=1)).strftime('%Y%m%d')
|
||
|
kwargs['remote'] = f'{remote}/{feed_date}'
|
||
|
|
||
|
resp = _search(**kwargs)
|
||
|
for r in _process_qsentry(resp):
|
||
|
yield r
|