From d498b334e31525c930b4f214e7222191644ee0ec Mon Sep 17 00:00:00 2001 From: ziirish Date: Thu, 28 Jan 2016 21:23:14 +0100 Subject: [PATCH] add: servers report (fixes #15) --- CHANGELOG.rst | 7 +- burpui/api/servers.py | 149 ++++++++++++++++++++++++-- burpui/routes.py | 6 ++ burpui/templates/gerard.js | 8 +- burpui/templates/js/servers-report.js | 94 ++++++++++++++++ burpui/templates/servers-report.html | 38 +++++++ burpui/templates/sidebar.html | 2 +- burpui/templates/small_topbar.html | 2 +- 8 files changed, 294 insertions(+), 12 deletions(-) create mode 100644 burpui/templates/js/servers-report.js create mode 100644 burpui/templates/servers-report.html diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6b0e3593..9394edc4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,13 +11,18 @@ Current - Add the ability to `chain multiple authentication backends `_ - Add display versions `within the interface `_ - Add support for `zip64 `_ -- Add Basic HTTP Authentication +- Add new `report `_ +- Add "restart" option to debian init script thanks to @Larsen +- Add Basic HTTP Authentication (mostly for the API) - Add full documented API - Fix issue `#81 `_ - Fix issue `#87 `_ - Fix issue `#88 `_ - Fix issue `#92 `_ - Fix issue `#95 `_ +- Fix issue `#99 `_ +- Fix issue `#100 `_ +- Fix issue `#101 `_ - `demo `_ - API refactoring - Security fixes diff --git a/burpui/api/servers.py b/burpui/api/servers.py index 57a9a704..20521ac8 100644 --- a/burpui/api/servers.py +++ b/burpui/api/servers.py @@ -56,22 +56,24 @@ class ServersStats(Resource): """ r = [] - if hasattr(api.bui.cli, 'servers'): # pragma: no cover - allowed = [] + if hasattr(api.bui.cli, 'servers'): + restrict = [] + check = False if (api.bui.acl and not api.bui.acl.is_admin(current_user.get_id())): - allowed = api.bui.acl.servers(current_user.get_id()) + check = True + restrict = api.bui.acl.servers(current_user.get_id()) - def get_servers_info(serv, output, allowed, username): + def get_servers_info(serv, output, restrict, check, username): try: - if allowed and serv in allowed: + if check and serv in restrict: output.put({ 'name': serv, 'clients': len(api.bui.acl.clients(username, serv)), 'alive': api.bui.cli.servers[serv].ping() }) return - elif not allowed: + elif not check: output.put({ 'name': serv, 'clients': len(api.bui.cli.servers[serv].get_all_clients(serv)), @@ -82,6 +84,139 @@ class ServersStats(Resource): except BUIserverException as e: output.put(str(e)) - r = parallel_loop(get_servers_info, api.bui.cli.servers, allowed, current_user.get_id()) + r = parallel_loop(get_servers_info, api.bui.cli.servers, restrict, check, current_user.get_id()) 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. + + 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) + @api.marshal_with(report_fields, code=200, description='Success') + @api.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 = {} + if hasattr(api.bui.cli, 'servers'): + restrict = [] + check = False + if (api.bui.acl and not + api.bui.acl.is_admin(current_user.get_id())): + check = True + restrict = api.bui.acl.servers(current_user.get_id()) + + stats = [] + + def get_servers_stats(serv, output, restrict, check, username): + try: + out = { + 'name': serv, + 'stats': { + 'total': 0, + 'totsize': 0, + 'linux': 0, + 'windows': 0, + 'unknown': 0, + }, + 'number': 0 + } + if check and serv not in restrict: + output.put(None) + return + clients = [] + if (api.bui.acl and not + api.bui.acl.is_admin(username)): + clients = [{'name': x} for x in api.bui.acl.clients(username, serv)] + else: + clients = api.bui.cli.get_all_clients(agent=serv) + + j = api.bui.cli.get_clients_report(clients, serv) + if 'clients' not in j or 'backups' not in j: + output.put(None) + return + for stats in j['clients']: + for key in ['total', 'totsize']: + out['stats'][key] += stats['stats'][key] + if stats['stats']['windows'] == 'true': + out['stats']['windows'] += 1 + elif stats['stats']['windows'] == 'false': + out['stats']['linux'] += 1 + else: + out['stats']['unknown'] += 1 + for bkp in j['backups']: + out['number'] += bkp['number'] + output.put(out) + except BUIserverException as e: + output.put(str(e)) + + stats = parallel_loop(get_servers_stats, api.bui.cli.servers, restrict, check, current_user.get_id()) + backups = [] + servers = [] + for serv in stats: + backups.append({'name': serv['name'], 'number': serv['number']}) + servers.append({'name': serv['name'], 'stats': serv['stats']}) + + r['backups'] = backups + r['servers'] = servers + return r diff --git a/burpui/routes.py b/burpui/routes.py index 63bc00b2..0d0826f8 100644 --- a/burpui/routes.py +++ b/burpui/routes.py @@ -210,6 +210,12 @@ def servers(): return render_template('servers.html', servers=True, overview=True) +@view.route('/servers-report', methods=['GET']) +@login_required +def servers_report(): + return render_template('servers-report.html', servers=True, report=True) + + @view.route('/login', methods=['POST', 'GET']) def login(): form = LoginForm(request.form) diff --git a/burpui/templates/gerard.js b/burpui/templates/gerard.js index 7ee4ad9a..ca9b13a0 100644 --- a/burpui/templates/gerard.js +++ b/burpui/templates/gerard.js @@ -210,6 +210,10 @@ $('#input-client').typeahead({ {% include "js/servers.js" %} {% endif -%} +{% if servers and report -%} +{% include "js/servers-report.js" %} +{% endif -%} + {% if clients and overview -%} {% include "js/clients.js" %} {% endif -%} @@ -303,7 +307,7 @@ $(function() { {% if not login -%} _check_running(); {% endif -%} - {% if servers and overview -%} + {% if servers -%} _servers(); {% endif -%} }); @@ -332,7 +336,7 @@ $(function() { {% if client and is_client_func -%} _client(); {% endif -%} - {% if servers and overview -%} + {% if servers -%} _servers(); {% endif -%} diff --git a/burpui/templates/js/servers-report.js b/burpui/templates/js/servers-report.js new file mode 100644 index 00000000..9f03066e --- /dev/null +++ b/burpui/templates/js/servers-report.js @@ -0,0 +1,94 @@ + +var _charts = [ 'repartition', 'size', 'files', 'backups' ]; +var _charts_obj = []; +var initialized = false; + +var _servers = function() { + if (!initialized) { + $.each(_charts, function(i, j) { + tmp = nv.models.pieChart() + .x(function(d) { return d.label }) + .y(function(d) { return d.value }) + .showLabels(true) + .labelThreshold(.05) + .labelType("percent") + .valueFormat(d3.format('f')) + .color(d3.scale.category20c().range()) + .labelThreshold(.05) + .donutRatio(0.55) + .donut(true) + ; + + tmp.tooltip.contentGenerator(function(obj) { return '

'+obj.data.label+'

'+(j == 'size' ? _bytes_human_readable(obj.data.value, false) : obj.data.value)+'

'; }); + + _charts_obj.push({ 'key': 'chart_'+j, 'obj': tmp, 'data': [] }); + }); + } + url = '{{ url_for("api.servers_report") }}'; + $.getJSON(url, function(d) { + rep = []; + size = []; + files = []; + rep = []; + backups = []; + $('.mycharts').each(function() { + $(this).parent().show(); + }); + $.each(d['servers'], function(k, s) { + size.push({'label': s.name, 'value': s.stats.totsize}); + files.push({'label': s.name, 'value': s.stats.total}); + if (s.stats.windows > 0) { + rep.push({'label': s.name + ' - Windows', 'value': s.stats.windows}); + } + if (s.stats.linux > 0) { + rep.push({'label': s.name + ' - Unix/Linux', 'value': s.stats.linux}); + } + if (s.stats.unknown > 0) { + rep.push({'label': s.name + ' - Unknown', 'value': s.stats.unknown}); + } + }); + $.each(d['backups'], function(k, b) { + backups.push({'label': b.name, 'value': b.number}); + }); + $.each(_charts_obj, function(i, c) { + switch (c.key) { + case 'chart_repartition': + c.data = rep; + break; + case 'chart_size': + c.data = size; + break; + case 'chart_files': + c.data = files; + break; + case 'chart_backups': + c.data = backups; + break; + } + }); + }) + .fail(myFail) + .fail(function () { + $('.mycharts').each(function() { + $(this).parent().hide(); + }); + }); + _redraw(); +}; + +var _redraw = function() { + $.each(_charts_obj, function(i, j) { + nv.addGraph(function() { + + d3.select('#'+j.key+' svg') + .datum(j.data) + .transition().duration(500) + .call(j.obj); + + nv.utils.windowResize(j.obj.update); + + return j.obj; + }); + }); + initialized = true; +}; diff --git a/burpui/templates/servers-report.html b/burpui/templates/servers-report.html new file mode 100644 index 00000000..d5499120 --- /dev/null +++ b/burpui/templates/servers-report.html @@ -0,0 +1,38 @@ +{% extends "layout.html" %} +{% block body %} + {% include "notifications.html" %} +
+ {% include "small_topbar.html" %} + +
+

Global report

+
+
+
+ +
+

Clients repartition per server

+
+
+
+ +
+

Used space

+
+
+
+ +
+

Total files

+
+
+
+ +
+

Number of backups

+
+
+
+{% endblock %} diff --git a/burpui/templates/sidebar.html b/burpui/templates/sidebar.html index a9f9b49a..6f8b2b9d 100644 --- a/burpui/templates/sidebar.html +++ b/burpui/templates/sidebar.html @@ -24,7 +24,7 @@ {% elif backup %}  Reports {% elif servers %} -  Reports +  Reports {% else %}  Reports {% endif %} diff --git a/burpui/templates/small_topbar.html b/burpui/templates/small_topbar.html index 113ac5e9..dc986315 100644 --- a/burpui/templates/small_topbar.html +++ b/burpui/templates/small_topbar.html @@ -16,7 +16,7 @@ {% elif clients -%}  Reports {% elif servers -%} -  Reports +  Reports {% elif backup -%}  Reports {% else -%}