...
 
Commits (26)
*.pyc
burpui-dev.cfg
devel.sh
*.egg*
dist
include LICENSE
include README.md
include VERSION
include burpui.cfg
include burp-ui.py
include share/burpui/etc/burpui.cfg
recursive-include burpui *
# Build Status
[![build status](http://ci.ziirish.me/projects/1/status.png?ref=master)](http://ci.ziirish.me/projects/1?ref=master)
# Requirements
For LDAP authentication (optional), we need the `simpleldap` module that
requires the following packages on Debian:
```
aptitude install libsasl2-dev libldap2-dev python-dev
```
Then we install the module itself:
```
pip install simpleldap
```
# Installation
Burp-UI is written in Python with the [Flask](http://flask.pocoo.org/) micro-framework.
The easiest way to install Flask is to use `pip`.
On Debian, you can install `pip` with the following command:
```
aptitude install python-pip
```
Once `pip` is installed, you can install `Flask` and the other requirements this
way:
```
pip install Flask
pip install flask-login
pip install WTForms
pip install Flask-WTF
```
Then you need to download the sources.
For example:
```
git clone http://git.ziirish.me/ziirish/burp-ui.git
```
You can setup various parameters in the [burpui.cfg](burpui.cfg) file.
Then you can run `burp-ui`: `python burp-ui.py`
By default, `burp-ui` listens on all interfaces (including IPv6) on port 5000.
You can then point your browser to http://127.0.0.1:5000/
# Notes
Please feel free to report any issues on my [gitlab](https://git.ziirish.me/ziirish/burp-ui/issues)
I have closed the *github tracker* to have a unique tracker system.
# TODO
Here is a non-exhaustive list of things I'd like to add:
* Authentication so that only admins can access to burp stats.
* server-initiated restoration (with burp, you can create a special file that triggers
a restoration when the client contacts the server the next time. In this case the
client must accepts server-initiated restoration).
* burp-server configuration front-end (so that you can configure your burp server
within burp-ui).
* More statistics.
* etc.
Also note that in the future, I'd like to write a burp-client GUI.
But I didn't think yet of what to do.
# Licenses
Burp-UI is released under the BSD 3-clause [License](LICENSE).
But this project is built on top of other tools listed here:
- [d3.js](http://d3js.org/) ([BSD](burpui/static/d3/LICENSE))
- [nvd3.js](http://nvd3.org/) ([Apache](burpui/static/nvd3/LICENSE.md))
- [jQuery](http://jquery.com/) ([MIT](burpui/static/jquery/MIT-LICENSE.txt))
- [jQuery-UI](http://jqueryui.com/) ([MIT](burpui/static/jquery-ui/MIT-LICENSE.txt))
- [fancytree](https://github.com/mar10/fancytree) ([MIT](burpui/static/fancytree/MIT-LICENSE.txt))
- [bootstrap](http://getbootstrap.com/) ([MIT](burpui/static/bootstrap/LICENSE))
- [typeahead](http://twitter.github.io/typeahead.js/) ([MIT](burpui/static/typeahead/LICENSE))
- [bootswatch](http://bootswatch.com/) ([MIT](burpui/static/bootstrap/bootswatch.LICENSE))
Also note that this project is made with the Awesome [Flask](http://flask.pocoo.org/) micro-framework.
# Thanks
Special Thanks to Graham Keeling for its great software! This project would not
exist without [Burp](http://burp.grke.org/).
......@@ -35,29 +35,20 @@ On Debian, you can install ``pip`` with the following command:
aptitude install python-pip
Once ``pip`` is installed, you can install ``Flask`` and the other requirements this
way:
Once ``pip`` is installed, you can install ``Burp-UI`` this way:
::
pip install Flask
pip install flask-login
pip install WTForms
pip install Flask-WTF
Then you need to download the sources.
For example:
::
git clone http://git.ziirish.me/ziirish/burp-ui.git
pip install burp-ui
You can setup various parameters in the `burpui.cfg`_ file.
This file can be specified with the ``-c`` flag or should be present in
``/etc/burp/burpui.cfg``.
By default ``Burp-UI`` ships with a default file located in
``$BURPUIDIR/../share/burpui/etc/burpui.cfg``.
Then you can run ``burp-ui``: ``python burp-ui.py``
Then you can run ``burp-ui``: ``burp-ui``
By default, ``burp-ui`` listens on all interfaces (including IPv6) on port 5000.
......@@ -89,14 +80,14 @@ Burp-UI is released under the BSD 3-clause `License`_.
But this project is built on top of other tools listed here:
- `d3.js <http://d3js.org/>`_ (`BSD <http://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/d3/LICENSE>`_)
- `nvd3.js <http://nvd3.org/>`_ (`Apache <http://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/nvd3/LICENSE.md>`_)
- `jQuery <http://jquery.com/>`_ (`MIT <http://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/jquery/MIT-LICENSE.txt>`_)
- `jQuery-UI <http://jqueryui.com/>`_ (`MIT <http://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/jquery-ui/MIT-LICENSE.txt>`_)
- `fancytree <https://github.com/mar10/fancytree>`_ (`MIT <http://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/fancytree/MIT-LICENSE.txt>`_)
- `bootstrap <http://getbootstrap.com/>`_ (`MIT <http://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/bootstrap/LICENSE>`_)
- `typeahead <http://twitter.github.io/typeahead.js/>`_ (`MIT <http://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/typeahead/LICENSE>`_)
- `bootswatch <http://bootswatch.com/>`_ (`MIT <http://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/bootstrap/bootswatch.LICENSE>`_)
- `d3.js <http://d3js.org/>`_ (`BSD <http://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/d3/LICENSE>`__)
- `nvd3.js <http://nvd3.org/>`_ (`Apache <http://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/nvd3/LICENSE.md>`__)
- `jQuery <http://jquery.com/>`_ (`MIT <http://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/jquery/MIT-LICENSE.txt>`__)
- `jQuery-UI <http://jqueryui.com/>`_ (`MIT <http://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/jquery-ui/MIT-LICENSE.txt>`__)
- `fancytree <https://github.com/mar10/fancytree>`_ (`MIT <http://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/fancytree/MIT-LICENSE.txt>`__)
- `bootstrap <http://getbootstrap.com/>`_ (`MIT <http://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/bootstrap/LICENSE>`__)
- `typeahead <http://twitter.github.io/typeahead.js/>`_ (`MIT <http://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/typeahead/LICENSE>`__)
- `bootswatch <http://bootswatch.com/>`_ (`MIT <http://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/bootstrap/bootswatch.LICENSE>`__)
Also note that this project is made with the Awesome `Flask`_ micro-framework.
......
#!/usr/bin/env python
# -*- coding: utf8 -*-
import sys
import os
import os.path
from optparse import OptionParser
sys.path.append('{0}/..'.format(os.path.join(os.path.dirname(os.path.realpath(__file__)))))
from burpui import app, bui
if __name__ == '__main__':
"""
Main function
"""
parser = OptionParser()
parser.add_option('-v', '--verbose', dest='log', help='verbose output', action='store_true')
parser.add_option('-c', '--config', dest='config', help='configuration file', metavar='CONFIG')
(options, args) = parser.parse_args()
d = options.log
app.config['DEBUG'] = d
if options.config:
if os.path.isfile(options.config):
conf = options.config
app.config['CFG'] = conf
else:
raise IOError('File not found: \'{0}\''.format(options.config))
else:
conf_files = ['/etc/burp/burpui.cfg', os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'share', 'burpui', 'etc', 'burpui.cfg')]
for p in conf_files:
app.logger.debug('Trying file \'%s\'', p)
if os.path.isfile(p):
app.config['CFG'] = p
app.logger.debug('Using file \'%s\'', p)
break
bui.setup(app.config['CFG'])
bui.run(d)
#!/usr/bin/env python
# -*- coding: utf8 -*-
import os
from optparse import OptionParser
from burpui import app, bui
if __name__ == '__main__':
"""
Main function
"""
parser = OptionParser()
parser.add_option('-v', '--verbose', dest='log', help='verbose output', action='store_true')
parser.add_option('-c', '--config', dest='config', help='configuration file', metavar='CONFIG')
(options, args) = parser.parse_args()
d = options.log
app.config['DEBUG'] = d
if options.config:
conf = options.config
else:
conf = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'burpui.cfg')
app.config['CFG'] = conf
bui.setup(conf)
bui.run(d)
bin/burp-ui
\ No newline at end of file
[Global]
# On which port is the application listening
port: 5000
# On which address is the application listening
# '::' is the default for all IPv6
bind: ::
# burp status port
bport: 4972
# burp status address
bhost: 127.0.0.1
# enable SSL
ssl: false
# ssl cert
sslcert: /etc/burp/ssl_cert-server.pem
# ssl key
sslkey: /etc/burp/ssl_cert-server.key
# burp server version (currently only burp 1.x is implemented)
version: 1
# authentication plugin (mandatory)
# list the misc/auth directory to see the available backends
auth: ldap
[UI]
# refresh interval of the pages in seconds
refresh: 15
# ldapauth specific options
[LDAP]
# LDAP host
host: 127.0.0.1
# LDAP filter to list existing users
#filter: &(burpui=1)
# LDAP base
base: ou=users,dc=example,dc=com
# Binddn to list existing users
binddn: cn=admin,dc=example,dc=com
# Bindpw to list existing users
bindpw: Sup3rS3cr3tPa$$w0rd
# basicauth specific options
#[BASIC]
#admin: password
#user1: otherpassword
share/burpui/etc/burpui.cfg
\ No newline at end of file
#!/usr/bin/env python
# -*- coding: utf8 -*-
"""
Burp-UI is a web-ui for burp backup written in python with Flask and
jQuery/Bootstrap
"""
__title__ = 'burp-ui'
__version__ = '0.0.1'
__author__ = 'Benjamin SANS (Ziirish)'
__license__ = 'BSD 3-clause'
import os
from flask import Flask
from flask.ext.login import LoginManager
from burpui.server import Server as BurpUI
# First, we setup the app
app = Flask(__name__)
app.config['CFG'] = os.path.join(app.root_path, '../burpui.cfg')
app.config['CFG'] = None
app.secret_key = 'VpgOXNXAgcO81xFPyWj07ppN6kExNZeCDRShseNzFKV7ZCgmW2/eLn6xSlt7pYAVBj12zx2Vv9Kw3Q3jd1266A=='
app.jinja_env.globals.update(isinstance=isinstance,list=list)
# We initialize the core
bui = BurpUI(app)
# And the login_manager
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
login_manager.login_message_category = 'info'
# Then we load our routes
import burpui.routes
# -*- coding: utf8 -*-
from flask_wtf import Form
from wtforms import TextField, PasswordField, validators
from wtforms import TextField, PasswordField, BooleanField, validators
class LoginForm(Form):
username = TextField('Username', [validators.Length(min=2, max=25)])
password = PasswordField('Password', [validators.Required()])
remember = BooleanField('Remember me', [validators.Optional()])
......@@ -2,7 +2,11 @@
from flask.ext.login import UserMixin
from burpui.misc.auth.interface import BUIhandler, BUIuser
import simpleldap
try:
import simpleldap
except ImportError:
raise ImportError('Unable to load \'simpleldap\' module')
import ConfigParser
class LdapLoader:
......@@ -18,8 +22,10 @@ class LdapLoader:
self.base = c.get('LDAP', 'base')
self.binddn = c.get('LDAP', 'binddn')
self.bindpw = c.get('LDAP', 'bindpw')
except ConfigParser.NoOptionError:
self.app.logger.error("Missing option")
except ConfigParser.NoOptionError, e:
self.app.logger.error(str(e))
except ConfigParser.NoSectionError, e:
self.app.logger.error(str(e))
self.app.logger.info('LDAP host: %s', self.host)
self.app.logger.info('LDAP filter: %s', self.filt)
......
......@@ -3,9 +3,13 @@ import re
import socket
import time
import datetime
import ConfigParser
from burpui.misc.utils import human_readable as _hr
from burpui.misc.backend.interface import BUIbackend
from burpui.misc.backend.interface import BUIbackend, BUIserverException
g_burpport = 4972
g_burphost = '127.0.0.1'
class Burp(BUIbackend):
states = {
......@@ -48,11 +52,25 @@ class Burp(BUIbackend):
'path'
]
def __init__(self, app=None, host='127.0.0.1', port=4972):
def __init__(self, app=None, host='127.0.0.1', port=4972, conf=None):
global g_burpport, g_burphost
self.app = app
self.host = host
self.port = port
self.running = []
if conf:
config = ConfigParser.ConfigParser({'bport': g_burpport, 'bhost': g_burphost})
with open(conf) as fp:
config.readfp(fp)
try:
self.port = config.getint('Burp1', 'bport')
self.host = config.get('Burp1', 'bhost')
except ConfigParser.NoOptionError, e:
self.app.logger.error(str(e))
except ConfigParser.NoSectionError, e:
self.app.logger.error(str(e))
self.app.logger.info('burp port: %d', self.port)
self.app.logger.info('burp host: %s', self.host)
"""
Utilities functions
......@@ -89,7 +107,7 @@ class Burp(BUIbackend):
return r
except socket.error:
self.app.logger.error('Cannot contact burp server at %s:%s', self.host, self.port)
return r
raise BUIserverException('Cannot contact burp server at {0}:{1}'.format(self.host, self.port))
def parse_backup_log(self, f, n, c=None):
"""
......@@ -230,7 +248,10 @@ class Burp(BUIbackend):
"""
if not name:
return False
f = self.status('c:{0}\n'.format(name))
try:
f = self.status('c:{0}\n'.format(name))
except BUIserverException:
return False
for line in f:
r = re.search('^{0}\s+\d\s+(\S)'.format(name), line)
if r and r.group(1) not in [ 'i', 'c', 'C' ]:
......@@ -243,7 +264,11 @@ class Burp(BUIbackend):
running a backup
"""
r = []
for c in self.get_all_clients():
try:
cls = self.get_all_clients()
except BUIserverException:
return r
for c in cls:
if self.is_backup_running(c['name']):
r.append(c['name'])
self.running = r
......
#raise NotImplementedError("Sorry, the current Backend does not implement this method!")
# -*- coding: utf8 -*-
class BUIbackend:
def __init__(self, app=None, host='127.0.0.1', port=4972):
......@@ -29,3 +29,6 @@ class BUIbackend:
def get_tree(self, name=None, backup=None, root=None):
raise NotImplementedError("Sorry, the current Backend does not implement this method!")
class BUIserverException(Exception):
pass
......@@ -7,49 +7,57 @@ from flask.ext.login import login_user, login_required, logout_user, current_use
from burpui import app, bui, login_manager
from burpui.forms import LoginForm
from burpui.misc.utils import human_readable as _hr
from burpui.misc.backend.interface import BUIserverException
@login_manager.user_loader
def load_user(userid):
return bui.uhandler.user(userid)
if bui.auth != 'none':
return bui.uhandler.user(userid)
return None
@app.route('/login', methods=['POST', 'GET'])
def login():
form = LoginForm(request.form)
if form.validate_on_submit():
user = bui.uhandler.user(form.username.data)
app.logger.info('%s active: %s', form.username.data, user.active)
if user.active and user.login(form.username.data, passwd=form.password.data):
login_user(user)
flash('Logged in successfully.')
return redirect(url_for('test_login'))
login_user(user, remember=form.remember.data)
flash('Logged in successfully', 'success')
return redirect(request.args.get("next") or url_for('home'))
return render_template('login.html', form=form, login=True)
@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('login'))
@app.route('/test_login')
@login_required
def test_login():
return render_template('test-login.html', login=True, user=current_user.name)
return redirect(url_for('home'))
"""
Here is the API
The whole API returns JSON-formated data
"""
@app.route('/api/running-clients.json')
@login_required
def running_clients():
"""
WebServer: return a list of running clients
API: running_clients
:returns: a list of running clients
"""
r = bui.cli.is_one_backup_running()
return jsonify(results=r)
@app.route('/api/render-live-template', methods=['GET'])
@app.route('/api/render-live-template/<name>')
@login_required
def render_live_tpl(name=None):
"""
API: render_live_tpl
:param name: the client name if any. You can also use the GET parameter
'name' to achieve the same thing
:returns: HTML that should be included directly into the page
"""
c = request.args.get('name')
if not name and not c:
abort(500)
......@@ -57,50 +65,73 @@ def render_live_tpl(name=None):
name = c
if name not in bui.cli.running:
abort(404)
counters = bui.cli.get_counters(name)
try:
counters = bui.cli.get_counters(name)
except BUIserverException:
counters = []
return render_template('live-monitor-template.html', cname=name, counters=counters)
@app.route('/api/live.json')
@login_required
def live():
"""
WebServer: return the live status of the server
API: live
:returns: the live status of the server
"""
r = []
for c in bui.cli.is_one_backup_running():
s = {}
s['client'] = c
s['status'] = bui.cli.get_counters(c)
try:
s['status'] = bui.cli.get_counters(c)
except BUIserverException:
s['status'] = []
r.append(s)
return jsonify(results=r)
@app.route('/api/running.json')
@login_required
def backup_running():
"""
WebService: return true if at least one backup is running
API: backup_running
:returns: true if at least one backup is running
"""
j = bui.cli.is_one_backup_running()
r = len(j) > 0
return jsonify(results=r)
@app.route('/api/client-tree.json/<name>/<int:backup>', methods=['GET'])
@login_required
def client_tree(name=None, backup=None):
"""
WebService: return a specific client files tree
:param name: the client name (mandatory)
:param backup: the backup number (mandatory)
"""
j = []
if not name or not backup:
return jsonify(results=j)
root = request.args.get('root')
j = bui.cli.get_tree(name, backup, root)
try:
j = bui.cli.get_tree(name, backup, root)
except BUIserverException, e:
err = [[2, str(e)]]
return jsonify(notif=err)
return jsonify(results=j)
@app.route('/api/clients-report.json')
@login_required
def clients_report_json():
"""
WebService: return a JSON with global stats
"""
j = []
clients = bui.cli.get_all_clients()
try:
clients = bui.cli.get_all_clients()
except BUIserverException, e:
err = [[2, str(e)]]
return jsonify(notif=err)
cl = []
ba = []
for c in clients:
......@@ -117,36 +148,57 @@ def clients_report_json():
@app.route('/api/client-stat.json/<name>')
@app.route('/api/client-stat.json/<name>/<int:backup>')
@login_required
def client_stat_json(name=None, backup=None):
"""
WebService: return a specific client detailed report
"""
j = []
if not name:
return jsonify(results=j)
err = [[1, 'No client defined']]
return jsonify(notif=err)
if backup:
f = bui.cli.status('c:{0}:b:{1}:f:log.gz\n'.format(name, backup))
try:
f = bui.cli.status('c:{0}:b:{1}:f:log.gz\n'.format(name, backup))
except BUIserverException, e:
err = [[2, str(e)]]
return jsonify(notif=err)
j = bui.cli.parse_backup_log(f, backup)
else:
for c in bui.cli.get_client(name):
try:
cl = bui.cli.get_client(name)
except BUIserverException, e:
err = [[2, str(e)]]
return jsonify(notif=err)
for c in cl:
f = bui.cli.status('c:{0}:b:{1}:f:log.gz\n'.format(name, c['number']))
j.append(bui.cli.parse_backup_log(f, c['number']))
return jsonify(results=j)
@app.route('/api/client.json/<name>')
@login_required
def client_json(name=None):
"""
WebService: return a specific client backups overview
"""
j = bui.cli.get_client(name)
try:
j = bui.cli.get_client(name)
except BUIserverException, e:
err = [[2, str(e)]]
return jsonify(notif=err)
return jsonify(results=j)
@app.route('/api/clients.json')
@login_required
def clients():
"""
WebService: return a JSON listing all clients
"""
j = bui.cli.get_all_clients()
try:
j = bui.cli.get_all_clients()
except BUIserverException, e:
err = [[2, str(e)]]
return jsonify(notif=err)
return jsonify(results=j)
"""
......@@ -181,17 +233,19 @@ And here is the main site
@app.route('/live-monitor')
@app.route('/live-monitor/<name>')
@login_required
def live_monitor(name=None):
"""
Live status monitor view
"""
if not bui.cli.running:
flash('Sorry, there are no running backups')
flash('Sorry, there are no running backups', 'warning')
return redirect(url_for('home'))
return render_template('live-monitor.html', live=True, cname=name)
@app.route('/client-browse/<name>', methods=['GET'])
@app.route('/client-browse/<name>/<int:backup>')
@login_required
def client_browse(name=None, backup=None):
"""
Browse a specific backup of a specific client
......@@ -202,6 +256,7 @@ def client_browse(name=None, backup=None):
return render_template('client-browse.html', tree=True, backup=True, overview=True, cname=name, nbackup=backup)
@app.route('/client-report/<name>')
@login_required
def client_report(name=None):
"""
Specific client report
......@@ -212,6 +267,7 @@ def client_report(name=None):
return render_template('client-report.html', client=True, report=True, cname=name)
@app.route('/clients-report')
@login_required
def clients_report():
"""
Global report
......@@ -220,6 +276,7 @@ def clients_report():
@app.route('/backup-report/<name>', methods=['GET'])
@app.route('/backup-report/<name>/<int:backup>', methods=['GET'])
@login_required
def backup_report(name=None, backup=None):
"""
Backup specific report
......@@ -230,6 +287,7 @@ def backup_report(name=None, backup=None):
@app.route('/client', methods=['GET'])
@app.route('/client/<name>')
@login_required
def client(name=None):
"""
Specific client overview
......@@ -243,6 +301,7 @@ def client(name=None):
return render_template('client.html', client=True, overview=True, cname=c)
@app.route('/')
@login_required
def home():
"""
Home page
......
......@@ -2,8 +2,6 @@
import ConfigParser
import sys
g_burpport = 4972
g_burphost = '127.0.0.1'
g_port = 5000
g_bind = '::'
g_refresh = 15
......@@ -11,7 +9,7 @@ g_ssl = False
g_sslcert = ''
g_sslkey = ''
g_version = 1
g_auth = 'ldap'
g_auth = 'basic'
class Server:
def __init__(self, app=None):
......@@ -19,19 +17,20 @@ class Server:
self.app = app
def setup(self, conf=None):
global g_refresh, g_burpport, g_burphost, g_port, g_bind, g_ssl, g_sslcert, g_sslkey, g_version, g_auth
global g_refresh, g_port, g_bind, g_ssl, g_sslcert, g_sslkey, g_version, g_auth
self.sslcontext = None
if not conf:
conf = self.app.config['CFG']
config = ConfigParser.ConfigParser({'bport': g_burpport, 'bhost': g_burphost, 'port': g_port,
'bind': g_bind, 'refresh': g_refresh, 'ssl': g_ssl, 'sslcert': g_sslcert,
if not conf:
raise IOError('No configuration file found')
config = ConfigParser.ConfigParser({'port': g_port,'bind': g_bind,
'refresh': g_refresh, 'ssl': g_ssl, 'sslcert': g_sslcert,
'sslkey': g_sslkey, 'version': g_version, 'auth': g_auth})
with open(conf) as fp:
config.readfp(fp)
try:
self.burpport = config.getint('Global', 'bport')
self.burphost = config.get('Global', 'bhost')
self.port = config.getint('Global', 'port')
self.bind = config.get('Global', 'bind')
self.vers = config.getint('Global', 'version')
......@@ -42,20 +41,23 @@ class Server:
self.ssl = False
self.sslcert = config.get('Global', 'sslcert')
self.sslkey = config.get('Global', 'sslkey')
try:
mod = __import__('burpui.misc.auth.{0}'.format(config.get('Global', 'auth')), fromlist=['UserHandler'])
UserHandler = mod.UserHandler
self.uhandler = UserHandler(self.app)
except Exception:
self.app.logger.error('Import Exception, module \'%s\'', config.get('Global', 'auth'))
sys.exit(1)
self.auth = config.get('Global', 'auth')
if self.auth != 'none':
try:
mod = __import__('burpui.misc.auth.{0}'.format(config.get('Global', 'auth')), fromlist=['UserHandler'])
UserHandler = mod.UserHandler
self.uhandler = UserHandler(self.app)
except Exception, e:
self.app.logger.error('Import Exception, module \'%s\': %s', config.get('Global', 'auth'), str(e))
sys.exit(1)
else:
# I know that's ugly, but hey, I need it!
self.app.login_manager._login_disabled = True
except ConfigParser.NoOptionError:
self.app.logger.error("Missing option")
self.app.config['REFRESH'] = config.getint('UI', 'refresh')
self.app.logger.info('burp port: %d', self.burpport)
self.app.logger.info('burp host: %s', self.burphost)
self.app.logger.info('burp version: %d', self.vers)
self.app.logger.info('listen port: %d', self.port)
self.app.logger.info('bind addr: %s', self.bind)
......@@ -67,9 +69,9 @@ class Server:
try:
mod = __import__('burpui.misc.backend.burp{0}'.format(self.vers), fromlist=['Burp'])
Client = mod.Burp
self.cli = Client(self.app, self.burphost, self.burpport)
except Exception:
self.app.logger.error('Failed loading backend for Burp version %d', self.vers)
self.cli = Client(self.app, conf=conf)
except Exception, e:
self.app.logger.error('Failed loading backend for Burp version %d: %s', self.vers, str(e))
sys.exit(2)
self.init = True
......
......@@ -203,3 +203,48 @@ svg text {
animation: blink 1s step-start 0s infinite;
-webkit-animation: blink 1s step-start 0s infinite;
}
.wrapper {
margin-top: 80px;
margin-bottom: 80px;
}
.form-signin {
background: linear-gradient(#8a9196, #7a8288 60%, #70787d);
max-width: 380px;
padding: 15px 35px 45px;
margin: 0 auto;
border: 1px solid rgba(0,0,0,0.1);
.checkbox {
margin-bottom: 30px;
}
.form-control {
position: relative;
font-size: 16px;
height: auto;
padding: 10px;
@include box-sizing(border-box);
&:focus {
z-index: 2;
}
}
input[type="text"] {
margin-bottom: -1px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
input[type="password"] {
margin-bottom: 20px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
}
#search {
margin-right: 0.1em;
}
{% 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;">
......
{% 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;">
......
{% 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;">
......
{% 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;">
......@@ -22,7 +23,7 @@
</tbody>
</table>
<div class="alert alert-dismissable alert-danger" id="client-alert" style="display: none;">
<strong>Sorry!</strong> There are no backups for this client.
<span class="glyphicon glyphicon-exclamation-sign"></span> <strong>Sorry!</strong> There are no backups for this client.
</div>
</div>
</div>
......
{% 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;">
......
{% 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;">
......
This diff is collapsed.
var _charts = [ 'new', 'changed', 'unchanged', 'deleted', 'total', 'scanned' ];
var _charts_obj = [];
var chart_unified;
var data_unified = [];
var initialized = false;
var _client = function() {
if (!initialized) {
$.each(_charts, function(i, j) {
tmp = nv.models.pieChart()
.x(function(d) { return d.label })
.y(function(d) { return parseInt(d.value) })
.showLabels(true)
.labelThreshold(.05)
.labelType("percent")
.donut(true)
.valueFormat(d3.format('f'))
.color(d3.scale.category20c().range())
.tooltipContent(function(key, y, e, graph) { return '<h3>'+key+'</h3><p>'+y+' '+j+'</p>'; })
.donutRatio(0.55);
_charts_obj.push({ 'key': 'chart_'+j, 'obj': tmp, 'data': [] });
});
chart_unified = nv.models.multiBarHorizontalChart()
.x(function(d) { return d.label })
.y(function(d) { return parseInt(d.value) })
.showValues(false)
.tooltips(true)
.valueFormat(d3.format('f'))
.tooltipContent(function(key, x, y, e, graph) { return '<h3>' + key + ' - ' + x + '</h3><p>' + y + '</p>'; })
.color(d3.scale.category20().range())
.showControls(false);
chart_unified.yAxis.tickFormat(d3.format(',.0f'));
}
url = '{{ url_for("client_stat_json", name=cname, backup=nbackup) }}';
$.getJSON(url, function(d) {
var _fields = [ 'dir', 'files', 'hardlink', 'softlink' ];
j = d.results;
if (!j) {
if (d.notif) {
$.each(d.notif, function(i, n) {
notif(n[0], n[1]);
});
}
$('.mycharts').each(function() {
$(this).parent().hide();
});
}
$('.mycharts').each(function() {
$(this).parent().show();
});
$.each(_charts, function(k, l) {
data = [];
$.each(_fields, function(i, c) {
if (j[c] !== undefined && parseInt(j[c][l]) != 0) {
data.push({ 'label': c, 'value': parseInt(j[c][l]) });
}
});
if (data.length > 0) {
var dis = (l === 'total' || l === 'unchanged' || l === 'scanned');
data_unified.push({ 'key': l, 'values': data, disabled: dis });
}
$.each(_charts_obj, function(i, c) {
if (c.key === 'chart_'+l) {
if (data.length > 0 && !j.windows) {
c.data = data;
$('#chart_'+l).parent().show();
} else {
$('#chart_'+l).parent().hide();
}
return false;
}
});
});
});
_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;
});
});
nv.addGraph(function() {
d3.select('#chart_combined svg')
.datum(data_unified)
.transition().duration(500)
.call(chart_unified);
nv.utils.windowResize(chart_unified.update);
});
initialized = true;
};
/***
* Here is our tree to browse a specific backup
* The tree is first initialized with the 'root' part of the backup.
* JSON example:
* {
* "results": [
* {
* "name": "/",
* "parent": "",
* "type": "d"
* }
* ]
* }
* This JSON is then parsed into another one to initialize our tree.
* Each 'directory' is expandable.
* A new JSON is returned for each one of then on-demand.
* JSON output:
* {
* "results": [
* {
* "name": "etc",
* "parent": "/",
* "type": "d"
* },
* {
* "name": "home",
* "parent": "/",
* "type": "d"
* }
* ]
* }
*/
$("#tree").fancytree({
extensions: ["glyph", "table", "gridnav", "filter"],
glyph: {
map: {
doc: "glyphicon glyphicon-file",
docOpen: "glyphicon glyphicon-file",
checkbox: "glyphicon glyphicon-unchecked",
checkboxSelected: "glyphicon glyphicon-check",
checkboxUnknown: "glyphicon glyphicon-share",
error: "glyphicon glyphicon-warning-sign",
expanderClosed: "glyphicon glyphicon-plus-sign",
expanderLazy: "glyphicon glyphicon-plus-sign",
// expanderLazy: "glyphicon glyphicon-expand",
expanderOpen: "glyphicon glyphicon-minus-sign",
// expanderOpen: "glyphicon glyphicon-collapse-down",
folder: "glyphicon glyphicon-folder-close",
folderOpen: "glyphicon glyphicon-folder-open",
loading: "glyphicon glyphicon-refresh"
// loading: "icon-spinner icon-spin"
}
},
source: function() {
r = [];
$.getJSON('{{ url_for("client_tree", name=cname, backup=nbackup) }}', function(data) {
if (!data.results) {
if (data.notif) {
$.each(data.notif, function(i, n) {
notif(n[0], n[1]);
});
}
return;
}
$.each(data.results, function(j, c) {
l = (c.type === "d");
f = (c.type === "d");
s = {title: c.name, key: c.name, lazy: l, folder: f, uid: c.uid, gid: c.gid, date: c.date, mode: c.mode, size: c.size, inodes: c.inodes};
r.push(s);
});
});
return r;
},
lazyLoad: function(event, data) {
var node = data.node;
// ugly hack to display a "loading" icon while retrieving data
node._isLoading = true;
node.renderStatus();
r = [];
p = node.key;
if (p !== "/") p += '/';
$.getJSON('{{ url_for("client_tree", name=cname, backup=nbackup) }}?root='+p, function(data) {
if (!data.results) {
if (data.notif) {
$.each(data.notif, function(i, n) {
notif(n[0], n[1]);
});
}
return;
}
$.each(data.results, function(j, c) {
l = (c.type === "d");
f = (c.type === "d");
s = {title: c.name, key: c.parent+c.name, lazy: l, folder: f, uid: c.uid, gid: c.gid, date: c.date, mode: c.mode, size: c.size, inodes: c.inodes};
r.push(s);
});
});
data.result = r;
node._isLoading = false;
node.renderStatus();
},
/*
// TODO: make it recursively loadable
loadChildren: function(event, data) {
data.node.visit(function(subNode){
if( subNode.isUndefined() && subNode.isExpanded() ) {
subNode.load();
}
});
},
*/
selectMode: 1,
scrollParent: $(window),
renderColumns: function(event, data) {
var node = data.node;
$tdList = $(node.tr).find(">td");
$tdList.eq(1).text(node.data.mode);
$tdList.eq(2).text(node.data.uid);
$tdList.eq(3).text(node.data.gid);
$tdList.eq(4).text(node.data.size);
$tdList.eq(5).text(node.data.date);
}
});
var tree = $("#tree").fancytree("getTree");
$("input[name=search-tree]").keyup(function(e){
var n,
leavesOnly = $("#leavesOnly").is(":checked"),
match = $(this).val();
if(e && e.which === $.ui.keyCode.ESCAPE || $.trim(match) === ""){
$("#btnResetSearch").click();
return;
}
if($("#regex").is(":checked")) {
// Pass function to perform match
n = tree.filterNodes(function(node) {
return new RegExp(match, "i").test(node.title);
}, leavesOnly);
} else {
// Pass a string to perform case insensitive matching
n = tree.filterNodes(match, leavesOnly);
}
$("#btnResetSearch").attr("disabled", false);
$("span#matches").text("(" + n + " matches)");
});
$("#btnResetSearch").click(function(e){
$("input[name=search-tree]").val("");
$("span#matches").text("");
tree.clearFilter();
}).attr("disabled", true);
$("input#hideMode").change(function(e){
tree.options.filter.mode = $(this).is(":checked") ? "hide" : "dimm";
tree.clearFilter();
$("input[name=search-tree]").keyup();
});
$("input#leavesOnly").change(function(e){
tree.clearFilter();
$("input[name=search-tree]").keyup();
});
$("input#regex").change(function(e){
tree.clearFilter();
$("input[name=search-tree]").keyup();
});
var _charts = [ 'new', 'changed', 'unchanged', 'deleted', 'total', 'scanned' ];
var _charts_obj = [];
var _chart_stats = null;
var _stats_data = [];
var initialized = false;
var _bytes_human_readable = function(bytes, si) {
var thresh = si ? 1000 : 1024;
if(bytes < thresh) return bytes + ' B';
var units = si ? ['kB','MB','GB','TB','PB','EB','ZB','YB'] : ['KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB'];
var u = -1;
do {
bytes /= thresh;
++u;
} while(bytes >= thresh);
return bytes.toFixed(1)+' '+units[u];
};
var _time_human_readable = function(d) {
str = '';
var days = Math.floor((d % 31536000) / 86400);
var hours = Math.floor(((d % 31536000) % 86400) / 3600);
var minutes = Math.floor((((d % 31536000) % 86400) % 3600) / 60);
var seconds = (((d % 31536000) % 86400) % 3600) % 60;
if (days > 0) {
str += days;
if (days > 1) {
str += ' days ';
} else {
str += ' day ';
}
}
if (hours > 0) {
str += pad(hours,2)+'H ';
}
str += pad(minutes,2)+'m '+pad(seconds,2)+'s';
return str;
};
var _client = function() {
if (!initialized) {
$.each(_charts, function(i, j) {
tmp = nv.models.stackedAreaChart()
.x(function(d) { return d[0] })
.y(function(d) { return d[1] })
.useInteractiveGuideline(true)
.color(d3.scale.category20c().range())
.margin({bottom: 105, left: 80})
;
tmp.xAxis.showMaxMin(true).tickFormat(function(d) { return d3.time.format('%x %X')(new Date(d)) }).rotateLabels(-45);
tmp.yAxis.tickFormat(d3.format('f'));
_charts_obj.push({ 'key': 'chart_'+j, 'obj': tmp, 'data': [] });
});
_chart_stats = nv.models.linePlusBarChart()
.color(d3.scale.category10().range())
.x(function(d,i) { return i })
.y(function(d) { return d[1] })
.margin({bottom: 105, left: 80})
;
_chart_stats.xAxis.tickFormat(function(d) {
var dx = data[0].values[d] && data[0].values[d][0] || 0;
return d3.time.format('%x %X')(new Date(dx))
}).rotateLabels(-45).showMaxMin(true);
_chart_stats.y1Axis.tickFormat(function(d) { return _time_human_readable(d) }); // Time
_chart_stats.y2Axis.tickFormat(function(d) { return _bytes_human_readable(d, false) }); // Size
_chart_stats.bars.forceY([0]);
}
url = '{{ url_for("client_stat_json", name=cname) }}';
$.getJSON(url, function(d) {
var _fields = [ 'dir', 'files', 'hardlink', 'softlink', 'files_enc', 'meta', 'meta_enc', 'special', 'efs', 'vssheader', 'vssheader_enc', 'vssfooter', 'vssfooter_enc' ];
var stats = true;
$.each(_charts, function(k, l) {
data = [];
$.each(_fields, function(i, c) {
values = [];
size = [];
duration = [];
push = false;
if (!d.results) {
if (d.notif) {
$.each(d.notif, function(i, n) {
notif(n[0], n[1]);
});
}
$('.mycharts').each(function() {
$(this).parent().hide();
});
return;
}
$('.mycharts').each(function() {
$(this).parent().show();
});
$.each(d.results, function(a, j) {
if (j[c] !== undefined) {
val = parseFloat(j[c][l]);
values.push([ parseInt(j.end)*1000, val ]);
push = true;
} else {
values.push([ parseInt(j.end), 0 ]);
}
if (stats) {
size.push([ parseInt(j.end)*1000, j.received ]);
duration.push([ parseInt(j.end)*1000, j.duration ]);
}
});
if (stats) {
stats = false;
_stats_data = [
{'key': 'Duration', 'bar': true, 'values': duration},
{'key': 'Bytes received', 'values': size}
]
}
if (push) {
data.push({ 'key': c, 'values': values });
}
});
$.each(_charts_obj, function(i, c) {
if (c.key === 'chart_'+l) {
if (data.length > 0) {
c.data = data;
$('#chart_'+l).parent().show();
} else {
$('#chart_'+l).parent().hide();
}
return false;
}
});
});
});
_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;
});
});
nv.addGraph(function() {
d3.select('#chart_stats svg')
.datum(_stats_data)
.transition().duration(500)
.call(_chart_stats);
nv.utils.windowResize(_chart_stats.update);
return _chart_stats;
});
initialized = true;
};
/***
* Here is the 'client' part
* It is available on the 'specific' client view
*/
/***
* _client: function that retrieve up-to-date informations from the burp server about a specific client
* JSON format:
* {
* "results": [
* {
* "date": "2014-05-12 19:40:02",
* "number": "254"
* },
* {
* "date": "2014-05-11 21:20:03",
* "number": "253"
* }
* ]
* }
* The JSON is then parsed into a table
*/
var _client = function() {
url = '{{ url_for("client_json", name=cname) }}';
$.getJSON(url, function(data) {
$('#table-client >tbody:last').empty();
$('#client-alert').hide();
if (!data.results) {
$('#table-client').hide();
$('#client-alert').show();
if (data.notif) {
$.each(data.notif, function(i, n) {
notif(n[0], n[1]);
});
}
return;
}
if (data.results.length == 0) {
$('#table-client').hide();
$('#client-alert').show();
} else {
$.each(data.results.reverse(), function(j, c) {
$('#table-client> tbody:last').append('<tr class="clickable" style="cursor: pointer;"><td><a href="{{ url_for("client_browse", name=cname) }}?backup='+c.number+'" style="color: inherit; text-decoration: inherit;">'+pad(c.number, 7)+'</a></td><td>'+c.date+'</td></tr>');
});
}
});