mirror of
https://github.com/ziirish/burp-ui.git
synced 2026-05-16 14:16:11 -06:00
add: new audit logging system (see #260 for details)
This commit is contained in:
parent
bb5ddcf2d9
commit
8dcc431a91
12 changed files with 368 additions and 15 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
0
burpui/misc/audit/__init__.py
Normal file
0
burpui/misc/audit/__init__.py
Normal file
97
burpui/misc/audit/basic.py
Normal file
97
burpui/misc/audit/basic.py
Normal 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)
|
||||
70
burpui/misc/audit/handler.py
Normal file
70
burpui/misc/audit/handler.py
Normal 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)
|
||||
75
burpui/misc/audit/interface.py
Normal file
75
burpui/misc/audit/interface.py
Normal 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
|
||||
|
|
@ -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']:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) 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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue