add: new audit logging system (see #260 for details)

This commit is contained in:
ziirish 2018-08-03 15:18:55 +02:00
parent bb5ddcf2d9
commit 8dcc431a91
No known key found for this signature in database
GPG key ID: 72DB229A64B54E46
12 changed files with 368 additions and 15 deletions

View file

@ -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

View file

@ -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:

View file

@ -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

View file

View file

@ -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)

View file

@ -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)

View file

@ -0,0 +1,75 @@
# -*- coding: utf8 -*-
"""
.. module:: burpui.misc.audit.interface
:platform: Unix
:synopsis: Burp-UI audit interface.
.. moduleauthor:: Ziirish <hi+burpui@ziirish.me>
"""
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

View file

@ -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']:

View file

@ -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
)

View file

@ -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

View file

@ -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
<gunicorn.html#sub-root-path>`__ 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) dont 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

View file

@ -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) dont 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]