Nenhuma Descrição

module_handler.py 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. # IRIS Source Code
  2. # Copyright (C) 2022 - DFIR IRIS Team
  3. # contact@dfir-iris.org
  4. # Copyright (C) 2021 - Airbus CyberSecurity (SAS)
  5. # ir@cyberactionlab.net
  6. #
  7. # This program is free software; you can redistribute it and/or
  8. # modify it under the terms of the GNU Lesser General Public
  9. # License as published by the Free Software Foundation; either
  10. # version 3 of the License, or (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  15. # Lesser General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU Lesser General Public License
  18. # along with this program; if not, write to the Free Software Foundation,
  19. # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  20. import traceback
  21. import base64
  22. import importlib
  23. from flask_login import current_user
  24. from packaging import version
  25. from pickle import dumps
  26. from pickle import loads
  27. from sqlalchemy import and_
  28. from app import app
  29. from app import celery
  30. from app import db
  31. from app.datamgmt.iris_engine.modules_db import get_module_config_from_hname
  32. from app.datamgmt.iris_engine.modules_db import iris_module_add
  33. from app.datamgmt.iris_engine.modules_db import iris_module_exists
  34. from app.datamgmt.iris_engine.modules_db import modules_list_pipelines
  35. from app.models.models import IrisHook
  36. from app.models.models import IrisModule
  37. from app.models.models import IrisModuleHook
  38. from app.util import hmac_sign
  39. from app.util import hmac_verify
  40. from iris_interface import IrisInterfaceStatus as IStatus
  41. log = app.logger
  42. def check_module_compatibility(module_version):
  43. return True
  44. def check_pipeline_args(pipelines_args):
  45. """
  46. Verify that the pipeline arguments are correct and can be used later on
  47. :param pipelines_args: JSON pipelines
  48. :return: Status
  49. """
  50. logs = []
  51. has_error = False
  52. if type(pipelines_args) != dict:
  53. return True, ["Error - Pipeline args are not json"]
  54. if not pipelines_args.get("pipeline_internal_name"):
  55. has_error = True
  56. logs.append("Error - pipeline_internal_name missing from pipeline config")
  57. if not pipelines_args.get("pipeline_human_name"):
  58. has_error = True
  59. logs.append("Error - pipeline_human_name missing from pipeline config")
  60. if not pipelines_args.get("pipeline_args"):
  61. has_error = True
  62. logs.append("Error - pipeline_args missing from pipeline config")
  63. if not pipelines_args.get("pipeline_update_support"):
  64. has_error = True
  65. logs.append("Error - pipeline_update_support missing from pipeline config")
  66. if not pipelines_args.get("pipeline_import_support"):
  67. has_error = True
  68. logs.append("Error - pipeline_import_support missing from pipeline config")
  69. return has_error, logs
  70. def check_module_health(module_instance):
  71. """
  72. Returns a status on the health of the module.
  73. A non healthy module will not be imported
  74. :param module_instance: Instance of the module to check
  75. :return: Status
  76. """
  77. logs = []
  78. def dup_logs(message):
  79. logs.append(message)
  80. log.info(message)
  81. if not module_instance:
  82. return False, ['Error - cannot instantiate the module. Check server logs']
  83. try:
  84. dup_logs("Testing module")
  85. dup_logs("Module name : {}".format(module_instance.get_module_name()))
  86. if type(module_instance.get_interface_version()) != str:
  87. mod_interface_version = str(module_instance.get_interface_version())
  88. else:
  89. mod_interface_version = module_instance.get_interface_version()
  90. if not (version.parse(app.config.get('MODULES_INTERFACE_MIN_VERSION'))
  91. <= version.parse(mod_interface_version)
  92. <= version.parse(app.config.get('MODULES_INTERFACE_MAX_VERSION'))):
  93. log.critical("Module interface no compatible with server. Expected "
  94. f"{app.config.get('MODULES_INTERFACE_MIN_VERSION')} <= module "
  95. f"<= {app.config.get('MODULES_INTERFACE_MAX_VERSION')}")
  96. logs.append("Module interface no compatible with server. Expected "
  97. f"{app.config.get('MODULES_INTERFACE_MIN_VERSION')} <= module "
  98. f"<= {app.config.get('MODULES_INTERFACE_MAX_VERSION')}")
  99. return False, logs
  100. dup_logs("Module interface version : {}".format(module_instance.get_interface_version()))
  101. module_type = module_instance.get_module_type()
  102. if module_type not in ["module_pipeline", "module_processor"]:
  103. log.critical(f"Unrecognised module type. Expected module_pipeline or module_processor, got {module_type}")
  104. logs.append(f"Unrecognised module type. Expected module_pipeline or module_processor, got {module_type}")
  105. return False, logs
  106. dup_logs("Module type : {}".format(module_instance.get_module_type()))
  107. if not module_instance.is_providing_pipeline() and module_type == 'pipeline':
  108. log.critical("Module of type pipeline has no pipelines")
  109. logs.append("Error - Module of type pipeline has not pipelines")
  110. return False, logs
  111. if module_instance.is_providing_pipeline():
  112. dup_logs("Module has pipeline : {}".format(module_instance.is_providing_pipeline()))
  113. # Check the pipelines config health
  114. has_error, llogs = check_pipeline_args(module_instance.pipeline_get_info())
  115. logs.extend(llogs)
  116. if has_error:
  117. return False, logs
  118. dup_logs("Module health validated")
  119. return module_instance.is_ready(), logs
  120. except Exception as e:
  121. log.exception("Error while checking module health")
  122. log.error(e.__str__())
  123. logs.append(e.__str__())
  124. return False, logs
  125. def instantiate_module_from_name(module_name):
  126. """
  127. Instantiate a module from a name. The method is not Exception protected.
  128. Caller need to take care of it failing.
  129. :param module_name: Name of the module to register
  130. :return: Class instance or None
  131. """
  132. try:
  133. mod_root_interface = importlib.import_module(module_name)
  134. if not mod_root_interface:
  135. return None
  136. except Exception as e:
  137. msg = f"Could not import root module {module_name}: {e}"
  138. log.error(msg)
  139. return None, msg
  140. # The whole concept is based on the fact that the root module provides an __iris_module_interface
  141. # variable pointing to the interface class with which Iris can talk to
  142. try:
  143. mod_interface = importlib.import_module("{}.{}".format(module_name,
  144. mod_root_interface.__iris_module_interface))
  145. except Exception as e:
  146. msg = f"Could not import module {module_name}: {e}"
  147. log.error(msg)
  148. return None, msg
  149. if not mod_interface:
  150. return None
  151. # Now get a handle on the interface class
  152. try:
  153. cl_interface = getattr(mod_interface, mod_root_interface.__iris_module_interface)
  154. except Exception as e:
  155. msg = f"Could not get handle on the interface class of module {module_name}: {e}"
  156. log.error(msg)
  157. return None, msg
  158. if not cl_interface:
  159. return None, ''
  160. # Try to instantiate the class
  161. try:
  162. mod_inst = cl_interface()
  163. except Exception as e:
  164. msg = f"Could not instantiate the class for module {module_name}: {e}"
  165. log.error(msg)
  166. return None, msg
  167. return mod_inst, 'Success'
  168. def configure_module_on_init(module_instance):
  169. """
  170. Configure a module after instantiation, with the current configuration
  171. :param module_instance: Instance of the module
  172. :return: IrisInterfaceStatus
  173. """
  174. if not module_instance:
  175. return IStatus.I2InterfaceNotImplemented('Module not found')
  176. return IStatus.I2ConfigureSuccess
  177. def preset_init_mod_config(mod_config):
  178. """
  179. Prefill the configuration with default one
  180. :param mod_config: Configuration
  181. :return: Tuple
  182. """
  183. index = 0
  184. for config in mod_config:
  185. if config.get('default') is not None:
  186. mod_config[index]["value"] = config.get('default')
  187. index += 1
  188. return mod_config
  189. def get_mod_config_by_name(module_name):
  190. """
  191. Returns a module configurationn based on its name
  192. :param: module_name: Name of the module
  193. :return: IrisInterfaceStatus
  194. """
  195. data = get_module_config_from_hname(module_name)
  196. if not data:
  197. return IStatus.I2InterfaceNotReady(message="Module not registered")
  198. return IStatus.I2Success(data=data)
  199. def register_module(module_name):
  200. """
  201. Register a module into IRIS
  202. :param module_name: Name of the module to register
  203. """
  204. if not module_name:
  205. log.error("Provided module has no names")
  206. return None, "Module has no names"
  207. try:
  208. mod_inst, _ = instantiate_module_from_name(module_name=module_name)
  209. if not mod_inst:
  210. log.error("Module could not be instantiated")
  211. return None, "Module could not be instantiated"
  212. if iris_module_exists(module_name=module_name):
  213. log.warning("Module already exists in Iris")
  214. return None, "Module already exists in Iris"
  215. # Auto parse the configuration and fill with default
  216. log.info('Parsing configuration')
  217. mod_config = preset_init_mod_config(mod_inst.get_init_configuration())
  218. log.info('Adding module')
  219. module = iris_module_add(module_name=module_name,
  220. module_human_name=mod_inst.get_module_name(),
  221. module_description=mod_inst.get_module_description(),
  222. module_config=mod_config,
  223. module_version=mod_inst.get_module_version(),
  224. interface_version=mod_inst.get_interface_version(),
  225. has_pipeline=mod_inst.is_providing_pipeline(),
  226. pipeline_args=mod_inst.pipeline_get_info(),
  227. module_type=mod_inst.get_module_type()
  228. )
  229. if module is None:
  230. return None, "Unable to register module"
  231. if mod_inst.get_module_type() == 'module_processor':
  232. mod_inst.register_hooks(module_id=module.id)
  233. except Exception as e:
  234. return None, "Fatal - {}".format(e.__str__())
  235. return module, "Module registered"
  236. def iris_update_hooks(module_name, module_id):
  237. """
  238. Update hooks upon settings update
  239. :param module_name: Name of the module to update
  240. :param module_id: ID of the module to update
  241. """
  242. if not module_name:
  243. log.error("Provided module has no names")
  244. return False, ["Module has no names"]
  245. try:
  246. mod_inst,_ = instantiate_module_from_name(module_name=module_name)
  247. if not mod_inst:
  248. log.error("Module could not be instantiated")
  249. return False, ["Module could not be instantiated"]
  250. if mod_inst.get_module_type() == 'module_processor':
  251. mod_inst.register_hooks(module_id=module_id)
  252. except Exception as e:
  253. return False, ["Fatal - {}".format(e.__str__())]
  254. return True, ["Module updated"]
  255. def register_hook(module_id: int, iris_hook_name: str, manual_hook_name: str = None,
  256. run_asynchronously: bool = True):
  257. """
  258. Register a new hook into IRIS. The hook_name should be a well-known hook to IRIS. iris_hooks table can be
  259. queried, or by default they are declared in iris source code > source > app > post_init.
  260. If is_manual_hook is set, the hook is triggered by user action and not automatically. If set, the iris_hook_name
  261. should be a manual hook (aka begin with on_manual_trigger_) otherwise an error is raised.
  262. If run_asynchronously is set (default), the action will be sent to RabbitMQ and processed asynchronously.
  263. If set to false, the action is immediately done, which means it needs to be quick otherwise the request will be
  264. pending and user experience degraded.
  265. :param module_id: Module ID to register
  266. :param iris_hook_name: Well-known hook name to register to
  267. :param manual_hook_name: The name of the hook displayed in the UI, if is_manual_hook is set
  268. :param run_asynchronously: Set to true to queue the module action in rabbitmq
  269. :return: Tuple
  270. """
  271. module = IrisModule.query.filter(IrisModule.id == module_id).first()
  272. if not module:
  273. return False, [f'Module ID {module_id} not found']
  274. is_manual_hook = False
  275. if "on_manual_trigger_" in iris_hook_name:
  276. is_manual_hook = True
  277. if not manual_hook_name:
  278. # Set default hook name
  279. manual_hook_name = f"{module.module_name}::{iris_hook_name}"
  280. hook = IrisHook.query.filter(IrisHook.hook_name == iris_hook_name).first()
  281. if not hook:
  282. return False, [f"Hook {iris_hook_name} is unknown"]
  283. if not isinstance(is_manual_hook, bool):
  284. return False, [f"Expected bool for is_manual_hook but got {type(is_manual_hook)}"]
  285. if not isinstance(run_asynchronously, bool):
  286. return False, [f"Expected bool for run_asynchronously but got {type(run_asynchronously)}"]
  287. mod = IrisModuleHook.query.filter(
  288. IrisModuleHook.hook_id == hook.id,
  289. IrisModuleHook.module_id == module_id,
  290. IrisModuleHook.manual_hook_ui_name == manual_hook_name
  291. ).first()
  292. if not mod:
  293. imh = IrisModuleHook()
  294. imh.is_manual_hook = is_manual_hook
  295. imh.wait_till_return = False
  296. imh.run_asynchronously = run_asynchronously
  297. imh.max_retry = 0
  298. imh.manual_hook_ui_name = manual_hook_name
  299. imh.hook_id = hook.id
  300. imh.module_id = module_id
  301. try:
  302. db.session.add(imh)
  303. db.session.commit()
  304. except Exception as e:
  305. return False, [str(e)]
  306. return True, [f"Hook {iris_hook_name} registered"]
  307. else:
  308. return True, [f"Hook {iris_hook_name} already registered"]
  309. def deregister_from_hook(module_id: int, iris_hook_name: str):
  310. """
  311. Deregister from an existing hook. The hook_name should be a well-known hook to IRIS. No error are thrown if the
  312. hook wasn't register in the first place
  313. :param module_id: Module ID to deregister
  314. :param iris_hook_name: hook_name to deregister from
  315. :return: IrisInterfaceStatus object
  316. """
  317. log.info(f'Deregistering module #{module_id} from {iris_hook_name}')
  318. hooks = IrisModuleHook.query.filter(
  319. IrisModuleHook.module_id == module_id,
  320. IrisHook.hook_name == iris_hook_name,
  321. IrisModuleHook.hook_id == IrisHook.id
  322. ).all()
  323. if hooks:
  324. for hook in hooks:
  325. log.info(f'Deregistered module #{module_id} from {iris_hook_name}')
  326. db.session.delete(hook)
  327. return True, ['Hook deregistered']
  328. @celery.task(bind=True)
  329. def task_hook_wrapper(self, module_name, hook_name, hook_ui_name, data, init_user, caseid):
  330. """
  331. Wrap a hook call into a Celery task to run asynchronously
  332. :param self: Task instance
  333. :param module_name: Module name to instanciate and call
  334. :param hook_name: Name of the hook which was triggered
  335. :param hook_ui_name: Name of the UI hook so module knows which hook was called
  336. :param data: Data associated to the hook to process
  337. :param init_user: User initiating the task
  338. :param caseid: Case associated
  339. :return: A task status JSON task_success or task_failure
  340. """
  341. try:
  342. # Data is serialized, so deserialized
  343. signature, pdata = data.encode("utf-8").split(b" ")
  344. is_verified = hmac_verify(signature, pdata)
  345. if is_verified is False:
  346. log.warning("data argument has not been correctly serialised")
  347. raise Exception('Unable to instantiate target module. Data has not been correctly serialised')
  348. deser_data = loads(base64.b64decode(pdata))
  349. except Exception as e:
  350. log.exception(e)
  351. raise Exception(e)
  352. try:
  353. _obj = None
  354. # The received object will most likely be cleared when handled by the task,
  355. # so we need to attach it to the session in the task
  356. _obj = []
  357. if isinstance(deser_data, list):
  358. _obj = []
  359. for dse_data in deser_data:
  360. obj = db.session.merge(dse_data)
  361. db.session.commit()
  362. _obj.append(obj)
  363. elif isinstance(deser_data, str) or isinstance(deser_data, int):
  364. _obj = [deser_data]
  365. elif isinstance(deser_data, dict):
  366. _obj = [deser_data]
  367. else:
  368. _obj_a = db.session.merge(deser_data)
  369. db.session.commit()
  370. _obj.append(_obj_a)
  371. except Exception as e:
  372. log.exception(e)
  373. raise Exception(e)
  374. log.info(f'Calling module {module_name} for hook {hook_name}')
  375. try:
  376. mod_inst, _ = instantiate_module_from_name(module_name=module_name)
  377. if mod_inst:
  378. task_status = mod_inst.hooks_handler(hook_name, hook_ui_name, data=_obj)
  379. # Recommit the changes made by the module
  380. db.session.commit()
  381. else:
  382. raise Exception('Unable to instantiate target module')
  383. except Exception as e:
  384. msg = f"Failed to run hook {hook_name} with module {module_name}. Error {str(e)}"
  385. log.critical(msg)
  386. log.exception(e)
  387. task_status = IStatus.I2Error(message=msg, logs=[traceback.format_exc()], user=init_user, caseid=caseid)
  388. return task_status
  389. def call_modules_hook(hook_name: str, data: any, caseid: int = None, hook_ui_name: str = None, module_name: str = None) -> any:
  390. """
  391. Calls modules which have registered the specified hook
  392. :raises: Exception if hook name doesn't exist. This shouldn't happen
  393. :param hook_name: Name of the hook to call
  394. :param hook_ui_name: UI name of the hook
  395. :param data: Data associated with the hook
  396. :param module_name: Name of the module to call. If None, all modules matching the hook will be called
  397. :param caseid: Case ID
  398. :return: Any
  399. """
  400. hook = IrisHook.query.filter(IrisHook.hook_name == hook_name).first()
  401. if not hook:
  402. log.critical(f'Hook name {hook_name} not found')
  403. raise Exception(f'Hook name {hook_name} not found')
  404. if hook_ui_name:
  405. condition = and_(
  406. IrisModule.is_active == True,
  407. IrisModuleHook.hook_id == hook.id,
  408. IrisModuleHook.manual_hook_ui_name == hook_ui_name
  409. )
  410. else:
  411. condition = and_(
  412. IrisModule.is_active == True,
  413. IrisModuleHook.hook_id == hook.id
  414. )
  415. if module_name:
  416. condition = and_(
  417. condition,
  418. IrisModule.module_name == module_name
  419. )
  420. modules = IrisModuleHook.query.with_entities(
  421. IrisModuleHook.run_asynchronously,
  422. IrisModule.module_name,
  423. IrisModuleHook.manual_hook_ui_name
  424. ).filter(condition).join(
  425. IrisModule, IrisModuleHook.module_id == IrisModule.id
  426. ).all()
  427. for module in modules:
  428. if module.run_asynchronously and "on_preload_" not in hook_name:
  429. log.info(f'Calling module {module.module_name} asynchronously for hook {hook_name} :: {hook_ui_name}')
  430. # We cannot directly pass the sqlalchemy in data, as it needs to be serializable
  431. # So pass a dumped instance and then rebuild on the task side
  432. ser_data = base64.b64encode(dumps(data))
  433. ser_data_auth = hmac_sign(ser_data) + b" " + ser_data
  434. task_hook_wrapper.delay(module_name=module.module_name, hook_name=hook_name,
  435. hook_ui_name=module.manual_hook_ui_name, data=ser_data_auth.decode("utf8"),
  436. init_user=current_user.name, caseid=caseid)
  437. else:
  438. # Direct call. Should be fast
  439. log.info(f'Calling module {module.module_name} for hook {hook_name}')
  440. try:
  441. was_list = True
  442. # The data passed on to the module hook is expected to be a list
  443. # So we make sure it's the case or adapt otherwise
  444. if not isinstance(data, list):
  445. data_list = [data]
  446. was_list = False
  447. else:
  448. data_list = data
  449. mod_inst, _ = instantiate_module_from_name(module_name=module.module_name)
  450. status = mod_inst.hooks_handler(hook_name, module.manual_hook_ui_name, data=data_list)
  451. except Exception as e:
  452. log.critical(f"Failed to run hook {hook_name} with module {module.module_name}. Error {str(e)}")
  453. continue
  454. if status.is_success():
  455. data_result = status.get_data()
  456. if not was_list:
  457. if not isinstance(data_result, list):
  458. log.critical(f"Error getting data result from hook {hook_name}: "
  459. f"A list is expected, instead got a {type(data_result)}")
  460. continue
  461. else:
  462. # We fetch the first elt here because we want to get back to the old type
  463. data = data_result[0]
  464. else:
  465. data = data_result
  466. return data
  467. def list_available_pipelines():
  468. """
  469. Return a list of available pipelines by requesting the DB
  470. """
  471. data = modules_list_pipelines()
  472. return data
  473. @celery.task(bind=True)
  474. def pipeline_dispatcher(self, module_name, hook_name, pipeline_type, pipeline_data, init_user, caseid):
  475. """
  476. Dispatch the pipelines according to their types
  477. :param pipeline_type: Type of pipeline
  478. :return: IrisInterfaceStatus
  479. """
  480. # Retrieve the handler
  481. mod, _ = instantiate_module_from_name(module_name=module_name)
  482. if mod:
  483. status = configure_module_on_init(mod)
  484. if status.is_failure():
  485. return status
  486. # This will run the task in the Celery context
  487. return mod.pipeline_handler(pipeline_type=pipeline_type,
  488. pipeline_data=pipeline_data)
  489. return IStatus.I2InterfaceNotImplemented("Couldn't instantiate module {}".format(module_name))