diff --git a/burpui/api/async.py b/burpui/api/async.py index 159e8ad7..fc1f759d 100644 --- a/burpui/api/async.py +++ b/burpui/api/async.py @@ -358,6 +358,11 @@ class AsyncRestore(Resource): strip = args['strip'] fmt = args['format'] or 'zip' passwd = args['pass'] + server_log = f' on {server}' if server else '' + args_log = args.copy() + # don't leak secrets in logs + del args_log['pass'] + bui.audit.logger.info(f'{current_user} requested restoration of backup n°{backup} for {name}{server_log}: {args_log}') room = None if WS_AVAILABLE: room = request.sid diff --git a/burpui/api/restore.py b/burpui/api/restore.py index 1c8108dc..c174a372 100644 --- a/burpui/api/restore.py +++ b/burpui/api/restore.py @@ -88,6 +88,11 @@ class Restore(Resource): stp = args['strip'] fmt = args['format'] or 'zip' pwd = args['pass'] + server_log = f' on {server}' if server else '' + args_log = args.copy() + # don't leak secrets in logs + del args_log['pass'] + bui.audit.logger.info(f'{current_user} requested restoration of backup n°{backup} for {name}{server_log}: {args_log}') resp = None # Check params if not lst or not name or not backup: diff --git a/burpui/engines/server.py b/burpui/engines/server.py index 93d38455..bcfd4b64 100644 --- a/burpui/engines/server.py +++ b/burpui/engines/server.py @@ -14,6 +14,7 @@ import logging from ..misc.auth.handler import UserAuthHandler from ..misc.acl.handler import ACLloader +from ..misc.audit.handler import BUIauditLoader from ..config import config from ..plugins import PluginManager @@ -30,7 +31,8 @@ BUI_DEFAULTS = { 'sslkey': '', 'backend': 'burp2', 'auth': ['basic'], - 'acl': 'none', + 'acl': ['none'], + 'audit': ['none'], 'prefix': '', 'plugins': [], 'demo': False, @@ -181,6 +183,11 @@ class BUIServer(Flask): 'string_lower_list' ) + self.audit_backends = self.config['BUI_AUDIT'] = self.conf.safe_get( + 'audit', + 'string_lower_list' + ) + # UI options self.config['REFRESH'] = self.conf.safe_get( 'refresh', @@ -345,6 +352,7 @@ class BUIServer(Flask): self.logger.info('refresh: {}'.format(self.config['REFRESH'])) self.logger.info('liverefresh: {}'.format(self.config['LIVEREFRESH'])) self.logger.info('auth: {}'.format(self.auth)) + self.logger.info('audit: {}'.format(self.audit_backends)) self.logger.info('celery: {}'.format(self.use_celery)) self.logger.info('redis: {}'.format(self.redis)) self.logger.info('limiter: {}'.format(self.limiter)) @@ -364,6 +372,14 @@ class BUIServer(Flask): if self.plugins: self.plugin_manager.load_all() + try: + self.audit = BUIauditLoader(self) + except ImportError as exc: + self.logger.critical( + f'Import Exception, module \'{self.audit_backends}\': {exc}' + ) + raise exc + if self.auth and 'none' not in self.auth: try: self.uhandler = UserAuthHandler(self) @@ -372,14 +388,14 @@ class BUIServer(Flask): 'Unable to load \'{}\' authentication backend:\n{}' .format(back, err) ) - except ImportError as e: + except ImportError as exc: self.logger.critical( 'Import Exception, module \'{0}\': {1}'.format( self.auth, - str(e) + str(exc) ) ) - raise e + raise exc self.acl_engine = self.config['BUI_ACL'] = self.conf.safe_get( 'acl', 'string_lower_list' @@ -394,14 +410,14 @@ class BUIServer(Flask): if self.acl_engine and 'none' not in self.acl_engine: try: self.acl_handler = ACLloader(self) - except Exception as e: + except Exception as exc: self.logger.critical( 'Import Exception, module \'{0}\': {1}'.format( self.acl_engine, - str(e) + str(exc) ) ) - raise e + raise exc else: self.acl_handler = False @@ -439,6 +455,8 @@ class BUIServer(Flask): else: raise Exception(msg) + self.audit.logger.info('Burp-UI server started') + @property def acl(self): """ACL module diff --git a/burpui/misc/audit/__init__.py b/burpui/misc/audit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/burpui/misc/audit/basic.py b/burpui/misc/audit/basic.py new file mode 100644 index 00000000..55f6acd9 --- /dev/null +++ b/burpui/misc/audit/basic.py @@ -0,0 +1,97 @@ +# -*- coding: utf8 -*- +import re +import logging + +from .interface import BUIaudit, BUIauditLogger as BUIauditLoggerInterface + + +class BUIauditLoader(BUIaudit): + section = name = 'BASIC:AUDIT' + + logfile = None + max_bytes = None + rotate = None + + def __init__(self, app): + """ + :param app: Instance of the app we are running in + :type app: :class:`burpui.engines.server.BUIServer` + """ + self.app = app + self.conf = self.app.conf + + self.level = default = logging.getLevelName(self.app.logger.getEffectiveLevel()) + + if self.section in self.conf.options: + self.priority = self.conf.safe_get( + 'priority', + 'integer', + self.section, + defaults=self.section + ) + self.level = self.conf.safe_get( + 'level', + section=self.section, + defaults=default + ) + self.logfile = self.conf.safe_get( + 'logfile', + section=self.section + ) + self.max_bytes = self.conf.safe_get( + 'max_bytes', + 'force_string', + section=self.section, + defaults='30 * 1024 * 1024' + ) + self.rotate = self.conf.safe_get( + 'rotate', + 'integer', + section=self.section, + defaults=5 + ) + + if self.max_bytes and re.match(r'(\d+\s*[+-/*]?\s*)+$', self.max_bytes): + self.max_bytes = eval(self.max_bytes) + else: + self.max_bytes = 0 + + if self.level != default: + self.level = logging.getLevelName(f'{self.level}'.upper()) + if not isinstance(self.level, int): + self.level = default + + self._logger = BUIauditLogger(self) + + @property + def logger(self): + return self._logger + + +class BUIauditLogger(BUIauditLoggerInterface): + _logger = logging.getLogger('burp-ui.audit') # type: logging.Logger + + def __init__(self, loader): + self.loader = loader + self._level = self.loader.level + LOG_FORMAT = '[%(asctime)s] AUDIT %(levelname)s: %(message)s' + + if self.loader.logfile and self.loader.logfile.lower() != 'none': + from logging.handlers import RotatingFileHandler + handler = RotatingFileHandler( + self.loader.logfile, + maxBytes=self.loader.max_bytes, + backupCount=self.loader.rotate + ) + else: + from logging import StreamHandler + handler = StreamHandler() + + handler.setLevel(self.level) + handler.setFormatter(logging.Formatter(LOG_FORMAT)) + + self._logger.setLevel(self.level) + self._logger.addHandler(handler) + + def log(self, level, message, *args, **kwargs): + self._logger.log(level, message, *args, **kwargs) diff --git a/burpui/misc/audit/handler.py b/burpui/misc/audit/handler.py new file mode 100644 index 00000000..8a1b6952 --- /dev/null +++ b/burpui/misc/audit/handler.py @@ -0,0 +1,70 @@ +# -*- coding: utf8 -*- +import os + +from .interface import BUIaudit, BUIauditLogger as BUIauditLoggerInterface + +from importlib import import_module +from collections import OrderedDict + + +class BUIauditLoader(BUIaudit): + """See :class:`burpui.misc.audit.interface.BUIaudit`""" + def __init__(self, app=None): + """See :func:`burpui.misc.audit.interface.BUIaudit.__init__` + + :param app: Instance of the app we are running in + :type app: :class:`burpui.engines.server.BUIServer` + """ + self.app = app + backends = [] + self.errors = {} + if self.app.audit_backends and 'none' not in self.app.audit_backends: + me, _ = os.path.splitext(os.path.basename(__file__)) + back = self.app.audit_backends + for au in back: + if au == me: + self.app.logger.critical('Recursive import not permitted!') + continue + try: + (modpath, _) = __name__.rsplit('.', 1) + mod = import_module('.' + au, modpath) + obj = mod.BUIauditLoader(self.app) + backends.append(obj) + except: + import traceback + self.errors[au] = traceback.format_exc() + for name, plugin in self.app.plugin_manager.get_plugins_by_type('audit').items(): + try: + obj = plugin.BUIauditLoader(self.app) + backends.append(obj) + except: + import traceback + self.errors[name] = traceback.format_exc() + backends.sort(key=lambda x: getattr(x, 'priority', -1), reverse=True) + if not backends and self.app.audit_backends and 'none' not in self.app.audit_backends: + raise ImportError( + 'No backend found for \'{}\':\n{}'.format(self.app.audit_backends, + self.errors) + ) + for name, err in self.errors.items(): + self.app.logger.error( + 'Unable to load module {}:\n{}'.format(repr(name), err) + ) + self.backends = OrderedDict() + for obj in backends: + self.backends[obj.name] = obj + self._logger = BUIauditLogger(self) + + @property + def logger(self): + return self._logger + + +class BUIauditLogger(BUIauditLoggerInterface): + + def __init__(self, loader): + self.loader = loader + + def log(self, level, message, *args, **kwargs): + for back in self.loader.backends.values(): + back.logger.log(level, message, *args, **kwargs) diff --git a/burpui/misc/audit/interface.py b/burpui/misc/audit/interface.py new file mode 100644 index 00000000..4bf01fa9 --- /dev/null +++ b/burpui/misc/audit/interface.py @@ -0,0 +1,75 @@ +# -*- coding: utf8 -*- +""" +.. module:: burpui.misc.audit.interface + :platform: Unix + :synopsis: Burp-UI audit interface. + +.. moduleauthor:: Ziirish + +""" +import logging + +from abc import ABCMeta, abstractmethod, abstractproperty + + +class BUIaudit(object, metaclass=ABCMeta): + """The :class:`burpui.misc.audit.interface.BUIaudit` class defines the audit + interface. + + :param app: Instance of the app we are running in + :type app: :class:`burpui.engines.server.BUIServer` + """ + + priority = 0 + + name = None + _logger = None + + def __init__(self, app): + self.app = app + + @abstractproperty + @property + def logger(self): + """:rtype: class:`BUIauditLogger`""" + return self._logger + + +class BUIauditLogger(object, metaclass=ABCMeta): + """The :class:`burpui.misc.audit.interface.BUIauditLogger` class defines the audit + Logger interface. + """ + + _level = -1 + + @property + def level(self): + return self._level + + def debug(self, message, *args, **kwargs): + if logging.DEBUG >= self.level: + self.log(logging.DEBUG, message, *args, **kwargs) + + def info(self, message, *args, **kwargs): + if logging.INFO >= self.level: + self.log(logging.INFO, message, *args, **kwargs) + + def warn(self, message, *args, **kwargs): + if logging.WARN >= self.level: + self.log(logging.WARN, message, *args, **kwargs) + + warning = warn + + def err(self, message, *args, **kwargs): + if logging.ERROR >= self.level: + self.log(logging.ERROR, message, *args, **kwargs) + + error = err + + def critical(self, message, *args, **kwargs): + if logging.CRITICAL >= self.level: + self.log(logging.CRITICAL, message, *args, **kwargs) + + @abstractmethod + def log(self, level, message, *args, **kwargs): + pass diff --git a/burpui/misc/auth/handler.py b/burpui/misc/auth/handler.py index aa9f5af0..dfa0bcfe 100644 --- a/burpui/misc/auth/handler.py +++ b/burpui/misc/auth/handler.py @@ -264,6 +264,10 @@ class UserHandler(BUIuser): def is_moderator(self): return self.acl.is_moderator() + @property + def backend(self): + return getattr(self.real, 'backend') + def _load_prefs(self): session['login'] = self.name if self.app.config['WITH_SQL']: diff --git a/burpui/misc/auth/interface.py b/burpui/misc/auth/interface.py index d7b9fbc7..42805638 100644 --- a/burpui/misc/auth/interface.py +++ b/burpui/misc/auth/interface.py @@ -120,10 +120,13 @@ class BUIuser(UserMixin, metaclass=ABCMeta): def __str__(self): msg = UserMixin.__str__(self) - return '{} (id: {}, admin: {}, authenticated: {}, active: {})'.format( + return '{} (id: {}, name: {}, backend: {}, admin: {}, moderator: {}, authenticated: {}, active: {})'.format( msg, self.get_id(), + self.name, + self.backend, self.is_admin, + self.is_moderator, self.is_authenticated, self.is_active ) diff --git a/burpui/misc/backend/interface.py b/burpui/misc/backend/interface.py index 9bd79b89..577c82e0 100644 --- a/burpui/misc/backend/interface.py +++ b/burpui/misc/backend/interface.py @@ -22,7 +22,7 @@ G_BURPCONFCLI = '/etc/burp/burp.conf' G_BURPCONFSRV = '/etc/burp/burp-server.conf' G_TMPDIR = '/tmp/bui' G_TIMEOUT = 15 -G_ZIP64 = False +G_ZIP64 = True G_INCLUDES = ['/etc/burp'] G_ENFORCE = False G_REVOKE = True diff --git a/docs/advanced_usage.rst b/docs/advanced_usage.rst index e41c4f7c..1959bed8 100644 --- a/docs/advanced_usage.rst +++ b/docs/advanced_usage.rst @@ -47,10 +47,14 @@ The `burpui.cfg`_ configuration file contains a ``[Global]`` section as follow: # you can also chain multiple backends. Example: "auth = ldap,basic" # the order will be respected unless you manually set a higher backend priority auth = basic - # acl plugin + # acl plugin (chainable, see 'auth' plugin option) # list misc/acl directory to see the available backends # default is no ACL acl = basic + # audit logger plugin (chainable, see 'auth' plugin option) + # list the misc/audit directory to see the available backends + # default is no audit log + audit = basic # You can change the prefix if you are behind a reverse-proxy under a custom # root path. For example: /burpui # You can also configure your reverse-proxy to announce the prefix through the @@ -64,8 +68,8 @@ The `burpui.cfg`_ configuration file contains a ``[Global]`` section as follow: Each option is commented, but here is a more detailed documentation: - *backend*: What `Burp`_ backend to load. Can either be one of *burp1*, - *burp2*, or *multi*, or can be whatever custom backend you like as long as it - implements the proper interface. + *burp2*, *async* or *multi*, or can be whatever custom backend you like as + long as it implements the proper interface. If providing a custom backend name, it must be located in the *plugins* directory. You can also specify a custom external module by providing the *dot-string* notation (example: *my.custom.backend*). @@ -73,6 +77,7 @@ Each option is commented, but here is a more detailed documentation: (see `Backends`_ for more details) - *auth*: What `Authentication`_ backend to use. - *acl*: What `ACL`_ module to use. +- *audit*: What `Audit`_ module to use. - *prefix*: You can host `Burp-UI`_ behind a sub-root path. See the `gunicorn `__ page for details. - *plugins*: Specify a list of paths to look for external plugins. See the @@ -208,7 +213,7 @@ tested: # enable zip64 feature. Python doc says: # « ZIP64 extensions are disabled by default because the default zip and unzip # commands on Unix (the InfoZIP utilities) don’t support these extensions. » - zip64 = false + zip64 = true These options are also available in the `bui-agent`_ configuration file. @@ -790,6 +795,59 @@ Is not the same as: +gp1 = user1 +Audit +----- + +`Burp-UI`_ implements some mechanisms to log *important* actions in a dedicated +logging target. + +- `Basic Audit`_ + +To disable the *audit* backend, set the *audit* option of the ``[Global]`` +section of your `burpui.cfg`_ file to *none*: + +:: + + [Global] + audit = none + +Basic Audit +^^^^^^^^^^^ + + +The *basic* audit backend can be enabled by setting the *audit* option of the +``[Global]`` section of your `burpui.cfg`_ file to *basic*: + +:: + + [Global] + audit = basic + + +Now you can add *basic audit* specific options: + +:: + + # Basic audit backend options + [BASIC:AUDIT] + # Backend priority. Higher is first + priority = 100 + # debug level (CRITICAL, ERROR, WARNING, INFO, DEBUG) + # the default is the same as your global application level + level = WARNING + # path to a file to log into + logfile = none + # maximum logfile size + max_bytes = 30 * 1024 * 1024 + # number of files to keep + rotate = 5 + + +.. note:: + The *basic* audit backend inherit the global application logger, so you may + see *duplicates* log entry depending of both your loggers debug level. + + .. _Burp: http://burp.grke.org/ .. _Burp-UI: https://git.ziirish.me/ziirish/burp-ui .. _burpui.cfg: https://git.ziirish.me/ziirish/burp-ui/blob/master/share/burpui/etc/burpui.sample.cfg diff --git a/share/burpui/etc/burpui.sample.cfg b/share/burpui/etc/burpui.sample.cfg index 1e38822c..72e964b9 100644 --- a/share/burpui/etc/burpui.sample.cfg +++ b/share/burpui/etc/burpui.sample.cfg @@ -20,10 +20,14 @@ single = true # you can also chain multiple backends. Example: "auth = ldap,basic" # the order will be respected unless you manually set a higher backend priority auth = basic -# acl plugin +# acl plugin (chainable, see 'auth' plugin option) # list misc/acl directory to see the available backends # default is no ACL acl = basic +# audit logger plugin (chainable, see 'auth' plugin option) +# list the misc/audit directory to see the available backends +# default is no audit log +audit = basic # you can change the prefix if you are behind a reverse-proxy under a custom # root path. For example: /burpui # You can also configure your reverse-proxy to announce the prefix through the @@ -153,7 +157,7 @@ appsecret = random # enable zip64 feature. Python doc says: # « ZIP64 extensions are disabled by default because the default zip and unzip # commands on Unix (the InfoZIP utilities) don’t support these extensions. » -zip64 = false +zip64 = true # disable server initiated restoration if `bconfcli` file contains # `server_can_restore = 0` noserverrestore = false @@ -322,6 +326,20 @@ noserverrestore = false ## As a result, user5 will be granted the following rights: ## '{"ro": {"agents": ["*", "agent1"], "www*": ["desk*"]}, "rw": {"clients": ["dev*"], "www*": ["desk1"]}} +## Basic audit backend options +#[BASIC:AUDIT] +## Backend priority. Higher is first +#priority = 100 +## debug level (CRITICAL, ERROR, WARNING, INFO, DEBUG) +## the default is the same as your global application level +#level = WARNING +## path to a file to log into +#logfile = none +## maximum logfile size +#max_bytes = 30 * 1024 * 1024 +## number of files to keep +#rotate = 5 + ## If you set backend to 'multi', add at least one section like this per ## bui-agent #[Agent:agent1]