add: servers report (fixes #15)

This commit is contained in:
ziirish 2016-01-28 21:23:14 +01:00
parent d3d7958060
commit d498b334e3
8 changed files with 294 additions and 12 deletions

View file

@ -11,13 +11,18 @@ Current
- Add the ability to `chain multiple authentication backends <https://git.ziirish.me/ziirish/burp-ui/issues/79>`_
- Add display versions `within the interface <https://git.ziirish.me/ziirish/burp-ui/issues/89>`_
- Add support for `zip64 <https://git.ziirish.me/ziirish/burp-ui/issues/97>`_
- Add Basic HTTP Authentication
- Add new `report <https://git.ziirish.me/ziirish/burp-ui/issues/15>`_
- 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 <https://git.ziirish.me/ziirish/burp-ui/issues/81>`_
- Fix issue `#87 <https://git.ziirish.me/ziirish/burp-ui/issues/87>`_
- Fix issue `#88 <https://git.ziirish.me/ziirish/burp-ui/issues/88>`_
- Fix issue `#92 <https://git.ziirish.me/ziirish/burp-ui/issues/92>`_
- Fix issue `#95 <https://git.ziirish.me/ziirish/burp-ui/issues/95>`_
- Fix issue `#99 <https://git.ziirish.me/ziirish/burp-ui/issues/99>`_
- Fix issue `#100 <https://git.ziirish.me/ziirish/burp-ui/issues/100>`_
- Fix issue `#101 <https://git.ziirish.me/ziirish/burp-ui/issues/101>`_
- `demo <https://demo.ziirish.me/>`_
- API refactoring
- Security fixes

View file

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

View file

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

View file

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

View file

@ -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 '<h3>'+obj.data.label+'</h3><p>'+(j == 'size' ? _bytes_human_readable(obj.data.value, false) : obj.data.value)+'</p>'; });
_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;
};

View file

@ -0,0 +1,38 @@
{% extends "layout.html" %}
{% block body %}
{% include "notifications.html" %}
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
{% include "small_topbar.html" %}
<ul class="breadcrumb" style="margin-bottom: 5px;">
<li class="active">Home</li>
</ul>
<br />
<h1 class="page-header">Global report</h1>
<div class="row placeholders">
<div class="col-xs-6 col-sm-6 placeholder">
<div class="mycharts" id="chart_repartition" style="height: 350px;">
<svg></svg>
</div>
<h4>Clients repartition per server</h4>
</div>
<div class="col-xs-6 col-sm-6 placeholder">
<div class="mycharts" id="chart_size" style="height: 350px;">
<svg></svg>
</div>
<h4>Used space</h4>
</div>
<div class="col-xs-6 col-sm-6 placeholder">
<div class="mycharts" id="chart_files" style="height: 350px;">
<svg></svg>
</div>
<h4>Total files</h4>
</div>
<div class="col-xs-6 col-sm-6 placeholder">
<div class="mycharts" id="chart_backups" style="height: 350px;">
<svg></svg>
</div>
<h4>Number of backups</h4>
</div>
</div>
</div>
{% endblock %}

View file

@ -24,7 +24,7 @@
{% elif backup %}
<a href="{{ url_for('view.backup_report', name=cname, backup=nbackup, server=server) }}"><span class="glyphicon glyphicon-stats"></span>&nbsp;Reports</a>
{% elif servers %}
<a href="{{ url_for('view.servers') }}"><span class="glyphicon glyphicon-stats"></span>&nbsp;Reports</a>
<a href="{{ url_for('view.servers_report') }}"><span class="glyphicon glyphicon-stats"></span>&nbsp;Reports</a>
{% else %}
<a href="#"><span class="glyphicon glyphicon-stats"></span>&nbsp;Reports</a>
{% endif %}

View file

@ -16,7 +16,7 @@
{% elif clients -%}
<a class="btn btn-default {% if report %}active{% endif %}" href="{{ url_for('view.clients_report', server=server) }}"><span class="glyphicon glyphicon-stats"></span>&nbsp;Reports</a>
{% elif servers -%}
<a class="btn btn-default {% if report %}active{% endif %}" href="{{ url_for('view.servers') }}"><span class="glyphicon glyphicon-stats"></span>&nbsp;Reports</a>
<a class="btn btn-default {% if report %}active{% endif %}" href="{{ url_for('view.servers_report') }}"><span class="glyphicon glyphicon-stats"></span>&nbsp;Reports</a>
{% elif backup -%}
<a class="btn btn-default {% if report %}active{% endif %}" href="{{ url_for('view.backup_report', name=cname, backup=nbackup, server=server) }}"><span class="glyphicon glyphicon-stats"></span>&nbsp;Reports</a>
{% else -%}