diff --git a/burpui/__init__.py b/burpui/__init__.py index ccd0dea1..34a09748 100644 --- a/burpui/__init__.py +++ b/burpui/__init__.py @@ -249,7 +249,7 @@ def init(conf=None, verbose=0, logfile=None, gunicorn=True, unittest=False, debu red = Redis(host=host, port=port) app.config['SESSION_TYPE'] = 'redis' app.config['SESSION_REDIS'] = red - app.config['SESSION_USE_SIGNER'] = app.secret_key != None + app.config['SESSION_USE_SIGNER'] = app.secret_key is not None app.config['SESSION_PERMANENT'] = False ses = Session() ses.init_app(app) diff --git a/burpui/api/__init__.py b/burpui/api/__init__.py index acfb93e5..ebec960b 100644 --- a/burpui/api/__init__.py +++ b/burpui/api/__init__.py @@ -12,13 +12,14 @@ import os import sys from flask import Blueprint, Response, request -from flask_restplus import Api +from flask_restplus import Api as ApiPlus from flask_login import current_user from flask_cache import Cache from importlib import import_module from functools import wraps from .._compat import IS_GUNICORN, PY3 +from ..exceptions import BUIserverException if PY3: # pragma: no cover basestring = str @@ -125,7 +126,7 @@ def api_login_required(func): return decorated_view -class ApiWrapper(Api): +class Api(ApiPlus): """Wrapper class around :class:`flask_restplus.Api`""" cache = Cache(config={'CACHE_TYPE': 'null', 'CACHE_NO_NULL_WARNING': True}) loaded = False @@ -157,4 +158,14 @@ class ApiWrapper(Api): apibp = Blueprint('api', __name__, url_prefix='/api') -api = ApiWrapper(apibp, title='Burp-UI API', description='Burp-UI API to interact with burp', doc='/doc', decorators=[api_login_required]) +api = Api(apibp, title='Burp-UI API', description='Burp-UI API to interact with burp', doc='/doc', decorators=[api_login_required]) + + +@api.errorhandler(BUIserverException) +def handle_bui_server_exception(error): + """Forward a BUIserverException to the final user + + :param error: Custom exception + :type error: :class:`burpui.exceptions.BUIserverException` + """ + return {'message': error.description}, error.code diff --git a/burpui/api/backup.py b/burpui/api/backup.py index 1fa3b431..90cffbee 100644 --- a/burpui/api/backup.py +++ b/burpui/api/backup.py @@ -143,11 +143,7 @@ class ServerBackup(Resource): (not api.bui.acl.is_client_allowed(self.username, name, server) and not - self.is_admin and - (to and not - api.bui.acl.is_client_allowed(self.username, - to, - server)))): + self.is_admin)): self.abort( 403, 'You are not allowed to schedule a backup for this client' diff --git a/burpui/api/restore.py b/burpui/api/restore.py index f5155e7f..0c6a5492 100644 --- a/burpui/api/restore.py +++ b/burpui/api/restore.py @@ -41,12 +41,12 @@ class Restore(Resource): - ``pass``: password to use for encrypted backups """ parser = api.parser() - parser.add_argument('pass', help='Password to use for encrypted backups', location=('form', 'json'), nullable=True) - parser.add_argument('format', required=False, help='Returning archive format', location=('form', 'json'), choices=('zip', 'tar.gz', 'tar.bz2'), default='zip', nullable=True) - parser.add_argument('strip', type=int, help='Number of elements to strip in the path', default=0, location=('form', 'json'), nullable=True) - parser.add_argument('list', required=True, help='List of files/directories to restore', location=('form', 'json'), nullable=False) + parser.add_argument('pass', help='Password to use for encrypted backups', nullable=True) + parser.add_argument('format', required=False, help='Returning archive format', choices=('zip', 'tar.gz', 'tar.bz2'), default='zip', nullable=True) + parser.add_argument('strip', type=int, help='Number of elements to strip in the path', default=0, nullable=True) + parser.add_argument('list', required=True, help='List of files/directories to restore', nullable=False) # FIXME: the example json seems interpreted during the raise of the exception - # parser.add_argument('list', required=True, help='List of files/directories to restore (example: \'{"restore":[{"folder":true,"key":"/etc"}]}\')', location=('form', 'json'), nullable=False) + # parser.add_argument('list', required=True, help='List of files/directories to restore (example: \'{"restore":[{"folder":true,"key":"/etc"}]}\')', nullable=False) @ns.expect(parser, validate=True) @ns.doc( @@ -230,11 +230,11 @@ class ServerRestore(Resource): - ``restoreto-sc``: restore files on an other client """ parser = api.parser() - parser.add_argument('list-sc', required=True, help='List of files/directories to restore', location='form', nullable=False) - parser.add_argument('strip-sc', type=int, help='Number of elements to strip in the path', default=0, location='form', nullable=True) - parser.add_argument('prefix-sc', help='Prefix to the restore path', location='form', nullable=True) - parser.add_argument('force-sc', type=boolean, help='Whether to overwrite existing files', default=False, location='form', nullable=True) - parser.add_argument('restoreto-sc', help='Restore files on an other client', location='form', nullable=True) + parser.add_argument('list-sc', required=True, help='List of files/directories to restore', nullable=False) + parser.add_argument('strip-sc', type=int, help='Number of elements to strip in the path', default=0, nullable=True) + parser.add_argument('prefix-sc', help='Prefix to the restore path', nullable=True) + parser.add_argument('force-sc', type=boolean, help='Whether to overwrite existing files', default=False, nullable=True) + parser.add_argument('restoreto-sc', help='Restore files on an other client', nullable=True) list_fields = api.model('ListRestoreFiles', { 'key': fields.String( diff --git a/burpui/api/servers.py b/burpui/api/servers.py index a4da333a..20c437ea 100644 --- a/burpui/api/servers.py +++ b/burpui/api/servers.py @@ -5,56 +5,55 @@ from . import api, cache_key, parallel_loop from .custom import fields, Resource from ..exceptions import BUIserverException -ns = api.namespace('servers', 'Servers methods') +if not api.bui.standalone: + ns = api.namespace('servers', 'Servers methods') + @ns.route('/stats', endpoint='servers_stats') + class ServersStats(Resource): + """The :class:`burpui.api.servers.ServersStats` resource allows you to + retrieve statistics about servers/agents. -@ns.route('/stats', endpoint='servers_stats') -class ServersStats(Resource): - """The :class:`burpui.api.servers.ServersStats` resource allows you to - retrieve statistics about servers/agents. - - This resource is part of the :mod:`burpui.api.servers` module. - """ - servers_fields = api.model('Servers', { - 'alive': fields.Boolean(required=True, description='Is the server reachable'), - 'clients': fields.Integer(required=True, description='Number of clients managed by this server'), - 'name': fields.String(required=True, description='Server name'), - }) - - @api.cache.cached(timeout=1800, key_prefix=cache_key) - @ns.marshal_list_with(servers_fields, code=200, description='Success') - @ns.doc( - responses={ - 500: 'Internal failure', - }, - ) - def get(self): - """Returns a list of servers (agents) with basic stats - - **GET** method provided by the webservice. - - The *JSON* returned is: - :: - - [ - { - 'alive': true, - 'clients': 2, - 'name': 'burp1', - }, - { - 'alive': false, - 'clients': 0, - 'name': 'burp2', - }, - ] - - - :returns: The *JSON* described above. + This resource is part of the :mod:`burpui.api.servers` module. """ + servers_fields = api.model('Servers', { + 'alive': fields.Boolean(required=True, description='Is the server reachable'), + 'clients': fields.Integer(required=True, description='Number of clients managed by this server'), + 'name': fields.String(required=True, description='Server name'), + }) - r = [] - if hasattr(api.bui.cli, 'servers'): + @api.cache.cached(timeout=1800, key_prefix=cache_key) + @ns.marshal_list_with(servers_fields, code=200, description='Success') + @ns.doc( + responses={ + 500: 'Internal failure', + }, + ) + def get(self): + """Returns a list of servers (agents) with basic stats + + **GET** method provided by the webservice. + + The *JSON* returned is: + :: + + [ + { + 'alive': true, + 'clients': 2, + 'name': 'burp1', + }, + { + 'alive': false, + 'clients': 0, + 'name': 'burp2', + }, + ] + + + :returns: The *JSON* described above. + """ + + r = [] restrict = [] check = False if api.bui.acl and not self.is_admin: @@ -83,80 +82,78 @@ class ServersStats(Resource): r = parallel_loop(get_servers_info, api.bui.cli.servers, restrict, check, self.username) - return r + return r + @ns.route('/report', endpoint='servers_report') + class ServersReport(Resource): + """The :class:`burpui.api.servers.ServersReport` resource allows you to + retrieve a report about servers/agents. -@ns.route('/report', endpoint='servers_report') -class ServersReport(Resource): - """The :class:`burpui.api.servers.ServersReport` resource allows you to - retrieve a report about servers/agents. - - This resource is part of the :mod:`burpui.api.servers` module. - """ - stats_fields = api.model('ServersStats', { - 'total': fields.Integer(required=True, description='Number of files', default=0), - 'totsize': fields.Integer(required=True, description='Total size occupied by all the backups of this server', default=0), - 'linux': fields.Integer(required=True, description='Total number of Linux/Unix clients on this server', default=0), - 'windows': fields.Integer(required=True, description='Total number of Windows clients on this server', default=0), - 'unknown': fields.Integer(required=True, description='Total number of Unknown clients on this server', default=0), - }) - server_fields = api.model('ServersReport', { - 'name': fields.String(required=True, description='Server name'), - 'stats': fields.Nested(stats_fields, required=True), - }) - backup_fields = api.model('ServersBackup', { - 'name': fields.String(required=True, description='Server name'), - 'number': fields.Integer(required=True, description='Number of backups on this server', default=0), - }) - report_fields = api.model('ServersReportFull', { - 'backups': fields.Nested(backup_fields, as_list=True, required=True), - 'servers': fields.Nested(server_fields, as_list=True, required=True), - }) - - @api.cache.cached(timeout=1800, key_prefix=cache_key) - @ns.marshal_with(report_fields, code=200, description='Success') - @ns.doc( - responses={ - 403: 'Insufficient permissions', - 500: 'Internal failure', - }, - ) - def get(self): - """Returns a global report about all the servers managed by Burp-UI - - **GET** method provided by the webservice. - - The *JSON* returned is: - :: - - { - "backups": [ - { - "name": "AGENT1", - "number": 49 - } - ], - "servers": [ - { - "name": "AGENT1", - "stats": { - "linux": 4, - "total": 349705, - "totsize": 119400711726, - "unknown": 0, - "windows": 1 - } - } - ] - } - - The output is filtered by the :mod:`burpui.misc.acl` module so that you - only see stats about the clients/servers you are authorized to. - - :returns: The *JSON* described above. + This resource is part of the :mod:`burpui.api.servers` module. """ - r = {} - if hasattr(api.bui.cli, 'servers'): + stats_fields = api.model('ServersStats', { + 'total': fields.Integer(required=True, description='Number of files', default=0), + 'totsize': fields.Integer(required=True, description='Total size occupied by all the backups of this server', default=0), + 'linux': fields.Integer(required=True, description='Total number of Linux/Unix clients on this server', default=0), + 'windows': fields.Integer(required=True, description='Total number of Windows clients on this server', default=0), + 'unknown': fields.Integer(required=True, description='Total number of Unknown clients on this server', default=0), + }) + server_fields = api.model('ServersReport', { + 'name': fields.String(required=True, description='Server name'), + 'stats': fields.Nested(stats_fields, required=True), + }) + backup_fields = api.model('ServersBackup', { + 'name': fields.String(required=True, description='Server name'), + 'number': fields.Integer(required=True, description='Number of backups on this server', default=0), + }) + report_fields = api.model('ServersReportFull', { + 'backups': fields.Nested(backup_fields, as_list=True, required=True), + 'servers': fields.Nested(server_fields, as_list=True, required=True), + }) + + @api.cache.cached(timeout=1800, key_prefix=cache_key) + @ns.marshal_with(report_fields, code=200, description='Success') + @ns.doc( + responses={ + 403: 'Insufficient permissions', + 500: 'Internal failure', + }, + ) + def get(self): + """Returns a global report about all the servers managed by Burp-UI + + **GET** method provided by the webservice. + + The *JSON* returned is: + :: + + { + "backups": [ + { + "name": "AGENT1", + "number": 49 + } + ], + "servers": [ + { + "name": "AGENT1", + "stats": { + "linux": 4, + "total": 349705, + "totsize": 119400711726, + "unknown": 0, + "windows": 1 + } + } + ] + } + + The output is filtered by the :mod:`burpui.misc.acl` module so that you + only see stats about the clients/servers you are authorized to. + + :returns: The *JSON* described above. + """ + r = {} restrict = [] check = False if api.bui.acl and not self.is_admin: @@ -217,4 +214,4 @@ class ServersReport(Resource): r['backups'] = backups r['servers'] = servers - return r + return r diff --git a/burpui/api/settings.py b/burpui/api/settings.py index 316dff8d..b1c6815a 100644 --- a/burpui/api/settings.py +++ b/burpui/api/settings.py @@ -9,6 +9,7 @@ """ from . import api from .custom import Resource +from .custom.inputs import boolean from .._compat import unquote from flask import jsonify, request, url_for from werkzeug.datastructures import ImmutableMultiDict @@ -28,6 +29,17 @@ class ServerSettings(Resource): This resource is part of the :mod:`burpui.api.settings` module. """ + @ns.doc( + params={ + 'conf': 'Path of the configuration file', + 'server': 'Which server to collect data from when in multi-agent mode', + }, + responses={ + 200: 'Success', + 403: 'Insufficient permissions', + 500: 'Internal failure', + } + ) def post(self, conf=None, server=None): """Saves the server configuration""" # Only the admin can edit the configuration @@ -37,6 +49,17 @@ class ServerSettings(Resource): noti = api.bui.cli.store_conf_srv(request.form, conf, server) return {'notif': noti}, 200 + @ns.doc( + params={ + 'conf': 'Path of the configuration file', + 'server': 'Which server to collect data from when in multi-agent mode', + }, + responses={ + 200: 'Success', + 403: 'Insufficient permissions', + 500: 'Internal failure', + } + ) def get(self, conf=None, server=None): """Reads the server configuration @@ -193,6 +216,16 @@ class ServerSettings(Resource): endpoint='clients_list') class ClientsList(Resource): + @ns.doc( + params={ + 'server': 'Which server to collect data from when in multi-agent mode', + }, + responses={ + 200: 'Success', + 403: 'Insufficient permissions', + 500: 'Internal failure', + } + ) def get(self, server=None): """Returns a list of clients""" # Only the admin can edit the configuration @@ -217,6 +250,21 @@ class ClientSettings(Resource): parser = api.parser() parser.add_argument('newclient', required=True, help="No 'newclient' provided") + parser_delete = api.parser() + parser_delete.add_argument('revoke', type=boolean, help='Whether to revoke the certificate or not', default=False, nullable=True) + parser_delete.add_argument('delcert', type=boolean, help='Whether to delete the certificate or not', default=False, nullable=True) + + @ns.doc( + params={ + 'server': 'Which server to collect data from when in multi-agent mode', + }, + responses={ + 200: 'Success', + 400: 'Missing parameter', + 403: 'Insufficient permissions', + 500: 'Internal failure', + } + ) def put(self, server=None): """Creates a new client""" # Only the admin can edit the configuration @@ -243,6 +291,18 @@ class ClientSettings(Resource): api.cache.clear() return {'notif': noti}, 201 + @ns.doc( + params={ + 'server': 'Which server to collect data from when in multi-agent mode', + 'client': 'Client name', + 'conf': 'Path of the configuration file', + }, + responses={ + 200: 'Success', + 403: 'Insufficient permissions', + 500: 'Internal failure', + } + ) def post(self, server=None, client=None, conf=None): """Saves a given client configuration""" # Only the admin can edit the configuration @@ -252,6 +312,18 @@ class ClientSettings(Resource): noti = api.bui.cli.store_conf_cli(request.form, client, conf, server) return {'notif': noti} + @ns.doc( + params={ + 'server': 'Which server to collect data from when in multi-agent mode', + 'client': 'Client name', + 'conf': 'Path of the configuration file', + }, + responses={ + 200: 'Success', + 403: 'Insufficient permissions', + 500: 'Internal failure', + } + ) def get(self, server=None, client=None, conf=None): """Reads a given client configuration""" # Only the admin can edit the configuration @@ -275,7 +347,19 @@ class ClientSettings(Resource): 'defaults': api.bui.cli.get_parser_attr('defaults', server) } + @ns.doc( + params={ + 'server': 'Which server to collect data from when in multi-agent mode', + 'client': 'Client name', + }, + responses={ + 200: 'Success', + 403: 'Insufficient permissions', + 500: 'Internal failure', + } + ) def delete(self, server=None, client=None): + """Deletes a given client""" # Only the admin can edit the configuration if api.bui.acl and not self.is_admin: self.abort(403, 'Sorry, you don\'t have rights to access the setting panel') @@ -295,6 +379,17 @@ class PathExpander(Resource): parser = api.parser() parser.add_argument('path', required=True, help="No 'path' provided") + @ns.doc( + params={ + 'server': 'Which server to collect data from when in multi-agent mode', + 'client': 'Client name', + }, + responses={ + 200: 'Success', + 403: 'Insufficient permissions', + 500: 'Internal failure', + } + ) def get(self, server=None, client=None): """Expends a given path @@ -313,10 +408,20 @@ class PathExpander(Resource): @ns.route('/options', - '/options', + '//options', endpoint='setting_options') class SettingOptions(Resource): + @ns.doc( + params={ + 'server': 'Which server to collect data from when in multi-agent mode', + }, + responses={ + 200: 'Success', + 403: 'Insufficient permissions', + 500: 'Internal failure', + } + ) def get(self, server=None): """Returns various setting options""" if api.bui.acl and not self.is_admin: