2016-08-12 11:31:23 +02:00
#!/usr/bin/env python3
#
# Core MISP expansion modules loader and web service
#
# Copyright (C) 2016 Alexandre Dulaunoy
# Copyright (C) 2016 CIRCL - Computer Incident Response Center Luxembourg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import signal
import sys
import importlib
import logging
import fnmatch
import argparse
import re
2016-08-25 17:36:28 +02:00
import datetime
2018-11-02 02:42:40 +01:00
import psutil
2016-08-12 11:31:23 +02:00
2024-01-06 12:11:13 +01:00
try :
import orjson as json
except ImportError :
import json
2016-08-24 00:22:03 +02:00
import tornado . web
2016-08-25 17:36:28 +02:00
import tornado . process
2016-08-24 00:22:03 +02:00
from tornado . ioloop import IOLoop
from tornado . concurrent import run_on_executor
from concurrent . futures import ThreadPoolExecutor
2024-01-09 12:19:48 +01:00
from pymisp import pymisp_json_default
2016-08-24 00:22:03 +02:00
2016-08-12 11:31:23 +02:00
try :
2018-12-11 15:29:09 +01:00
from . modules import * # noqa
2016-08-12 11:31:23 +02:00
HAS_PACKAGE_MODULES = True
except Exception as e :
2021-12-24 15:33:23 +01:00
logging . exception ( e )
2016-08-12 11:31:23 +02:00
HAS_PACKAGE_MODULES = False
try :
2018-12-11 15:29:09 +01:00
from . helpers import * # noqa
2016-08-12 11:31:23 +02:00
HAS_PACKAGE_HELPERS = True
except Exception as e :
2021-12-24 15:33:23 +01:00
logging . exception ( e )
2016-08-12 11:31:23 +02:00
HAS_PACKAGE_HELPERS = False
log = logging . getLogger ( ' misp-modules ' )
2016-08-12 12:35:33 +02:00
2016-08-12 11:31:23 +02:00
def handle_signal ( sig , frame ) :
2019-04-26 11:35:03 +02:00
IOLoop . instance ( ) . add_callback_from_signal ( IOLoop . instance ( ) . stop )
2016-08-12 11:31:23 +02:00
2023-07-02 12:01:46 +02:00
def init_logger ( debug = False ) :
2016-08-12 11:31:23 +02:00
formatter = logging . Formatter ( ' %(asctime)s - %(name)s - %(levelname)s - %(message)s ' )
2023-07-02 12:01:46 +02:00
handler = logging . StreamHandler ( )
2016-08-12 11:31:23 +02:00
handler . setFormatter ( formatter )
2024-01-08 22:07:51 +01:00
# Enable access logs
access_log = logging . getLogger ( ' tornado.access ' )
access_log . propagate = False
access_log . setLevel ( logging . INFO )
access_log . addHandler ( handler )
# Set application log
2016-08-12 11:31:23 +02:00
log . addHandler ( handler )
2023-07-02 12:01:46 +02:00
log . propagate = False
log . setLevel ( logging . DEBUG if debug else logging . INFO )
2016-08-12 11:31:23 +02:00
def load_helpers ( helpersdir ) :
sys . path . append ( helpersdir )
hhandlers = { }
helpers = [ ]
for root , dirnames , filenames in os . walk ( helpersdir ) :
if os . path . basename ( root ) == ' __pycache__ ' :
continue
if re . match ( r ' ^ \ . ' , os . path . basename ( root ) ) :
continue
for filename in fnmatch . filter ( filenames , ' *.py ' ) :
if filename == ' __init__.py ' :
continue
helpername = filename . split ( " . " ) [ 0 ]
hhandlers [ helpername ] = importlib . import_module ( helpername )
selftest = hhandlers [ helpername ] . selftest ( )
if selftest is None :
helpers . append ( helpername )
2024-01-06 11:59:22 +01:00
log . info ( f ' Helpers loaded { filename } ' )
2016-08-12 11:31:23 +02:00
else :
2024-01-06 14:20:10 +01:00
log . warning ( f ' Helpers failed { filename } due to { selftest } ' )
2016-08-12 11:31:23 +02:00
def load_package_helpers ( ) :
if not HAS_PACKAGE_HELPERS :
2024-01-06 14:20:10 +01:00
log . error ( ' Unable to load MISP helpers from package. ' )
2024-01-06 11:59:22 +01:00
sys . exit ( 1 )
2016-08-12 11:31:23 +02:00
mhandlers = { }
helpers = [ ]
for path , helper in sys . modules . items ( ) :
if not path . startswith ( ' misp_modules.helpers. ' ) :
continue
2024-01-06 11:59:22 +01:00
helper_name = path . replace ( ' misp_modules.helpers. ' , ' ' )
mhandlers [ helper_name ] = helper
selftest = mhandlers [ helper_name ] . selftest ( )
2016-08-12 12:35:33 +02:00
if selftest is None :
2024-01-06 11:59:22 +01:00
helpers . append ( helper_name )
log . info ( f ' Helper loaded { helper_name } ' )
2016-08-12 12:35:33 +02:00
else :
2024-01-06 14:20:10 +01:00
log . warning ( f ' Helpers failed { helper_name } due to { selftest } ' )
2016-08-12 11:31:23 +02:00
return mhandlers , helpers
def load_modules ( mod_dir ) :
sys . path . append ( mod_dir )
mhandlers = { }
modules = [ ]
for root , dirnames , filenames in os . walk ( mod_dir ) :
if os . path . basename ( root ) == ' __pycache__ ' :
continue
if os . path . basename ( root ) . startswith ( " . " ) :
continue
for filename in fnmatch . filter ( filenames , ' *.py ' ) :
2016-11-15 16:43:11 +01:00
if root . split ( ' / ' ) [ - 1 ] . startswith ( ' _ ' ) :
continue
2016-08-12 11:31:23 +02:00
if filename == ' __init__.py ' :
continue
2024-01-06 11:59:22 +01:00
module_name = filename . split ( " . " ) [ 0 ]
module_type = os . path . split ( mod_dir ) [ 1 ]
2016-08-12 11:31:23 +02:00
try :
2024-01-06 11:59:22 +01:00
mhandlers [ module_name ] = importlib . import_module ( os . path . basename ( root ) + ' . ' + module_name )
2016-08-12 11:31:23 +02:00
except Exception as e :
2024-01-06 11:59:22 +01:00
log . warning ( f ' MISP modules { module_name } failed due to { e } ' )
2016-08-12 11:31:23 +02:00
continue
2024-01-06 11:59:22 +01:00
modules . append ( module_name )
log . info ( f ' MISP modules { module_name } imported ' )
mhandlers [ ' type: ' + module_name ] = module_type
2016-08-12 11:31:23 +02:00
return mhandlers , modules
def load_package_modules ( ) :
if not HAS_PACKAGE_MODULES :
2024-01-06 14:20:10 +01:00
log . error ( ' Unable to load MISP modules from package. ' )
2024-01-06 11:59:22 +01:00
sys . exit ( 1 )
2016-08-12 11:31:23 +02:00
mhandlers = { }
modules = [ ]
for path , module in sys . modules . items ( ) :
2018-12-11 15:29:09 +01:00
r = re . findall ( r " misp_modules[.]modules[.]( \ w+)[.]([^_] \ w+) " , path )
2016-08-12 11:31:23 +02:00
if r and len ( r [ 0 ] ) == 2 :
2024-01-06 11:59:22 +01:00
module_type , module_name = r [ 0 ]
mhandlers [ module_name ] = module
modules . append ( module_name )
log . info ( f ' MISP modules { module_name } imported ' )
mhandlers [ ' type: ' + module_name ] = module_type
2016-08-12 11:31:23 +02:00
return mhandlers , modules
2024-01-06 13:07:10 +01:00
class Healthcheck ( tornado . web . RequestHandler ) :
def get ( self ) :
self . write ( b ' { " status " : true} ' )
2016-08-12 11:31:23 +02:00
class ListModules ( tornado . web . RequestHandler ) :
2018-12-11 15:29:09 +01:00
global loaded_modules
global mhandlers
2016-08-12 11:31:23 +02:00
def get ( self ) :
ret = [ ]
2024-01-06 14:20:10 +01:00
for module_name in loaded_modules :
2024-01-06 13:07:10 +01:00
ret . append ( {
2024-01-06 14:20:10 +01:00
' name ' : module_name ,
' type ' : mhandlers [ ' type: ' + module_name ] ,
' mispattributes ' : mhandlers [ module_name ] . introspection ( ) ,
' meta ' : mhandlers [ module_name ] . version ( )
2024-01-06 13:07:10 +01:00
} )
2016-08-12 11:31:23 +02:00
log . debug ( ' MISP ListModules request ' )
self . write ( json . dumps ( ret ) )
2016-08-24 00:22:03 +02:00
class QueryModule ( tornado . web . RequestHandler ) :
2016-08-25 17:36:28 +02:00
# Default value in Python 3.5
# https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.ThreadPoolExecutor
nb_threads = tornado . process . cpu_count ( ) * 5
executor = ThreadPoolExecutor ( nb_threads )
2016-08-23 18:02:29 +02:00
2016-08-24 00:22:03 +02:00
@run_on_executor
2024-01-06 14:58:47 +01:00
def run_request ( self , module_name , json_payload , dict_payload ) :
log . debug ( ' MISP QueryModule %s request %s ' , module_name , json_payload )
module = mhandlers [ module_name ]
if getattr ( module , " dict_handler " , None ) :
# New method that avoids double JSON decoding, new modules should define dict_handler
response = module . dict_handler ( request = dict_payload )
else :
response = module . handler ( q = json_payload )
2024-01-09 12:19:48 +01:00
return json . dumps ( response , default = pymisp_json_default )
2016-08-23 18:02:29 +02:00
2016-08-21 10:21:00 +02:00
@tornado.gen.coroutine
2016-08-12 11:31:23 +02:00
def post ( self ) :
2016-08-23 18:02:29 +02:00
try :
2024-01-06 12:11:13 +01:00
json_payload = self . request . body
dict_payload = json . loads ( json_payload )
2016-08-25 17:36:28 +02:00
if dict_payload . get ( ' timeout ' ) :
timeout = datetime . timedelta ( seconds = int ( dict_payload . get ( ' timeout ' ) ) )
else :
2018-06-29 06:02:08 +02:00
timeout = datetime . timedelta ( seconds = 300 )
2024-01-06 14:58:47 +01:00
future = self . run_request ( dict_payload [ ' module ' ] , json_payload , dict_payload )
response = yield tornado . gen . with_timeout ( timeout , future )
2016-08-25 17:36:28 +02:00
self . write ( response )
except tornado . gen . TimeoutError :
2024-01-06 12:11:13 +01:00
log . warning ( ' Timeout on {} ' . format ( dict_payload [ ' module ' ] ) )
2016-08-25 17:36:28 +02:00
self . write ( json . dumps ( { ' error ' : ' Timeout. ' } ) )
2016-08-23 18:02:29 +02:00
except Exception :
2016-08-25 17:36:28 +02:00
self . write ( json . dumps ( { ' error ' : ' Something went wrong, look in the server logs for details ' } ) )
2024-01-06 14:20:10 +01:00
log . exception ( ' Something went wrong when processing query request ' )
2016-08-25 17:36:28 +02:00
finally :
self . finish ( )
2016-08-12 11:31:23 +02:00
2016-08-12 12:35:33 +02:00
2019-04-26 13:48:38 +02:00
def _launch_from_current_dir ( ) :
log . info ( ' Launch MISP modules server from current directory. ' )
os . chdir ( os . path . dirname ( __file__ ) )
modulesdir = ' modules '
helpersdir = ' helpers '
load_helpers ( helpersdir = helpersdir )
return load_modules ( modulesdir )
2016-08-12 11:31:23 +02:00
def main ( ) :
2016-08-12 12:35:33 +02:00
global mhandlers
global loaded_modules
2016-08-12 11:31:23 +02:00
signal . signal ( signal . SIGINT , handle_signal )
signal . signal ( signal . SIGTERM , handle_signal )
2024-01-06 11:59:22 +01:00
arg_parser = argparse . ArgumentParser ( description = ' misp-modules server ' , formatter_class = argparse . RawTextHelpFormatter )
2024-01-08 22:07:51 +01:00
arg_parser . add_argument ( ' -t ' , ' --test ' , default = False , action = ' store_true ' , help = ' Test mode ' )
arg_parser . add_argument ( ' -s ' , ' --system ' , default = False , action = ' store_true ' , help = ' Run a system install (package installed via pip) ' )
arg_parser . add_argument ( ' -d ' , ' --debug ' , default = False , action = ' store_true ' , help = ' Enable debugging ' )
arg_parser . add_argument ( ' -p ' , ' --port ' , default = 6666 , help = ' misp-modules TCP port (default 6666) ' )
arg_parser . add_argument ( ' -l ' , ' --listen ' , default = ' localhost ' , help = ' misp-modules listen address (default localhost) ' )
2024-01-06 11:59:22 +01:00
arg_parser . add_argument ( ' -m ' , default = [ ] , action = ' append ' , help = ' Register a custom module ' )
arg_parser . add_argument ( ' --devel ' , default = False , action = ' store_true ' , help = ''' Start in development mode, enable debug, start only the module(s) listed in -m. \n Example: -m misp_modules.modules.expansion.bgpranking ''' )
args = arg_parser . parse_args ( )
2019-04-26 13:48:38 +02:00
if args . devel :
2024-01-08 22:07:51 +01:00
init_logger ( debug = True )
2024-01-06 11:59:22 +01:00
log . info ( ' Launch MISP modules server in development mode. Enable debug, load a list of modules is -m is used. ' )
2019-04-26 13:48:38 +02:00
if args . m :
mhandlers = { }
modules = [ ]
for module in args . m :
splitted = module . split ( " . " )
modulename = splitted [ - 1 ]
moduletype = splitted [ 2 ]
mhandlers [ modulename ] = importlib . import_module ( module )
mhandlers [ ' type: ' + modulename ] = moduletype
modules . append ( modulename )
2024-01-06 11:59:22 +01:00
log . info ( f ' MISP modules { modulename } imported ' )
2019-04-26 13:48:38 +02:00
else :
mhandlers , loaded_modules = _launch_from_current_dir ( )
2016-08-12 11:31:23 +02:00
else :
2024-01-08 22:07:51 +01:00
init_logger ( debug = args . debug )
if args . system :
2019-04-26 13:48:38 +02:00
log . info ( ' Launch MISP modules server from package. ' )
load_package_helpers ( )
mhandlers , loaded_modules = load_package_modules ( )
else :
mhandlers , loaded_modules = _launch_from_current_dir ( )
for module in args . m :
mispmod = importlib . import_module ( module )
mispmod . register ( mhandlers , loaded_modules )
2018-12-11 15:29:09 +01:00
2024-01-06 13:07:10 +01:00
service = [
( r ' /modules ' , ListModules ) ,
( r ' /query ' , QueryModule ) ,
( r ' /healthcheck ' , Healthcheck ) ,
]
2016-08-12 11:31:23 +02:00
application = tornado . web . Application ( service )
2018-11-02 02:42:40 +01:00
try :
2024-01-08 22:07:51 +01:00
application . listen ( args . port , address = args . listen )
2018-11-02 02:42:40 +01:00
except Exception as e :
if e . errno == 98 :
pids = psutil . pids ( )
for pid in pids :
p = psutil . Process ( pid )
if p . name ( ) == " misp-modules " :
print ( " \n \n \n " )
print ( e )
print ( " \n misp-modules is still running as PID: {} \n " . format ( pid ) )
print ( " Please kill accordingly: " )
print ( " sudo kill {} " . format ( pid ) )
2024-01-06 11:59:22 +01:00
return 1
2018-11-02 02:42:40 +01:00
print ( e )
print ( " misp-modules might still be running. " )
2024-01-09 12:19:23 +01:00
else :
log . exception ( f " Could not listen on { args . listen } : { args . port } " )
return 1
2018-11-02 02:42:40 +01:00
2024-01-08 22:07:51 +01:00
log . info ( f ' MISP modules server started on { args . listen } port { args . port } ' )
if args . test :
2016-08-12 11:31:23 +02:00
log . info ( ' MISP modules started in test-mode, quitting immediately. ' )
2024-01-06 11:59:22 +01:00
return 0
2019-04-26 11:35:03 +02:00
try :
IOLoop . instance ( ) . start ( )
finally :
IOLoop . instance ( ) . stop ( )
2016-08-12 11:31:23 +02:00
return 0
if __name__ == ' __main__ ' :
sys . exit ( main ( ) )