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
 |