...
 
Commits (348)
......@@ -30,6 +30,6 @@ indent_style = tab
indent_style = space
indent_size = 4
[.gitlab-ci.yml}]
[.gitlab-ci.yml]
indent_style = space
indent_size = 2
......@@ -13,6 +13,7 @@ dist
_build
.tags
celerybeat-schedule
Pipfile*
pkgs/burp-ui-sql/burpui_sql/VERSION
pkgs/burp-ui-extra/burpui_extra/VERSION
pkgs/burp-ui-agent/burpui_agent
before_script:
- git submodule update --init
variables:
GIT_SUBMODULE_STRATEGY: recursive
stages:
- test
......@@ -28,18 +28,6 @@ test:py2.7:
except:
- tags
test:py3.4:
stage: test
image: python:3.4
script:
- pip install tox
- tox -e py34
tags:
- docker
except:
- tags
- demo
test:py3.6:
stage: test
image: python:3.6
......@@ -50,21 +38,22 @@ test:py3.6:
- docker
except:
- tags
- demo
build:py2:
stage: build
image: python:2.7
script:
- /bin/bash tests/run_build.sh
tags:
- build
only:
- master
- demo
- master@ziirish/burp-ui
- demo@ziirish/burp-ui
artifacts:
paths:
- dist/
- meta/
expire_in: 2 mos
build:py3:
stage: build
......@@ -74,59 +63,105 @@ build:py3:
tags:
- build
only:
- master
- master@ziirish/burp-ui
artifacts:
paths:
- dist/
- meta/
expire_in: 2 mos
build:doc:
stage: build
image: python:3.6
script:
- pip install -U -r docs/requirements.txt
- make doc
tags:
- build
only:
- master@ziirish/burp-ui
artifacts:
paths:
- docs/_build/html
expire_in: 2 mos
allow_failure: true
build:docker:latest:
stage: build
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- docker build -t $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest -f docker/Dockerfile .
- docker build -t $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest-py3.6 -f docker/Dockerfile-py3.6 .
- cd docker/demo/docker-pg && docker build -t $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/pgsql:latest .
- docker build --pull -t $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest -f docker/Dockerfile .
- (cd docker/demo/docker-pg && docker build --pull -t $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/pgsql:10 .)
- (cd docker/components/docker-burp && docker build --pull -t $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/burp:2.0.54 .)
- docker push $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest
- docker push $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest-py3.6
- docker push $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/pgsql:latest
- docker push $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/pgsql:10
- docker push $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/burp:2.0.54
tags:
- registry
only:
- rc
- rc@ziirish/burp-ui
build:docker:release:
stage: build
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- docker build -t $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:$CI_COMMIT_TAG -f docker/Dockerfile .
- docker build -t $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:$CI_COMMIT_TAG-py3.6 -f docker/Dockerfile-py3.6 .
- cd docker/demo/docker-pg && docker build -t $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/pgsql:$CI_COMMIT_TAG .
- docker build --pull -t $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:$CI_COMMIT_TAG -f docker/Dockerfile .
- (cd docker/demo/docker-pg && docker build --pull -t $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/pgsql:10 .)
- (cd docker/components/docker-burp && docker build --pull -t $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/burp:2.0.54 .)
- docker push $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:$CI_COMMIT_TAG
- docker push $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:$CI_COMMIT_TAG-py3.6
- docker push $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/pgsql:$CI_COMMIT_TAG
- docker push $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/pgsql:10
- docker push $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/burp:2.0.54
only:
- tags
tags:
- registry
build:docker:stable:
stage: build
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- docker build --pull -t $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:stable -f docker/Dockerfile .
- (cd docker/demo/docker-pg && docker build --pull -t $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/pgsql:10 .)
- (cd docker/components/docker-burp && docker build --pull -t $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/burp:2.0.54 .)
- docker push $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:stable
- docker push $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/pgsql:10
- docker push $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/burp:2.0.54
only:
- stable@ziirish/burp-ui
tags:
- registry
build:docker:demo:
stage: build
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- docker build --pull -t $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:demo -f docker/Dockerfile .
- docker push $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:demo
- "curl $SENTRY_WEBHOOK -X POST -H 'Content-Type: application/json' -d '{\"version\": \"'$CI_COMMIT_REF_NAME'_'$CI_COMMIT_SHA'\"}'"
only:
- demo@ziirish/burp-ui
tags:
- registry
deploy:demo:
stage: deploy
script:
- find docker/demo/ -name "install" | xargs sed -i "s/@build@/$(git rev-parse HEAD)/"
- find docker/demo/ -name "install" -o -name "init" | xargs sed -i "s/@build@/$CI_COMMIT_SHA/"
- cd docker/demo/ && find . -maxdepth 1 -type d -a ! -name dist -exec cp -r ../../dist "{}/" \; -exec cp -r ../../meta "{}/" \; && cd ../..
- find docker/demo/ -name "Dockerfile" | xargs sed -i "s,^.*@ARTIFACTS@.*$,COPY dist/*.tar.gz /tmp/burpui.dev.tar.gz,;s,^.*@BUIAGENT_ARTIFACTS@.*$,COPY meta/burp-ui-agent*.tar.gz /tmp/burp-ui-agent.dev.tar.gz,"
- test -d /srv/demo/docker && rm -rf /srv/demo/docker
- cp -r docker/demo/ /srv/demo/docker
- cd /srv/demo/docker/
- docker-compose build
# old docker client, we need the "-e" flag
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN -e $DUMMY_EMAIL $CI_REGISTRY
- docker-compose build --pull
- docker-compose stop
- docker-compose rm -f
- docker-compose up -d
tags:
- deploy
only:
- demo
- demo@ziirish/burp-ui
environment:
name: demo
url: https://demo.burp-ui.org/
......@@ -40,17 +40,22 @@ Unable to login: SQL error
```
$ burp -v
burp-2.0.54
burp-2.1.18
```
# Sysinfo
```
$ bui-manage sysinfo
Python version: 3.6.1
Burp-UI version: 0.5.0 (stable)
Single mode: True
Backend version: 2
Python version: 3.6.1
Burp-UI version: 0.5.0 (stable)
Single mode: True
Backend version: 2
WebSocket embedded: False
WebSocket available: True
Config file: share/burpui/etc/burpui.sample.cfg
Burp client version: 2.1.18
Burp server version: 2.1.18
```
# Steps to reproduce
......
[submodule "burpui/static/vendor"]
path = burpui/static/vendor
url = https://git.ziirish.me/ziirish/burp-ui-externals.git
url = ../../ziirish/burp-ui-externals.git
Changelog
=========
0.5.0 (09/05/2017)
0.6.0 (05/14/2018)
------------------
- **BREAKING**: the *BASIC* ``ACL`` engine will now grant users on all agents if they are not explicitly defined
- **BREAKING**: a new ``[ACL]`` section has been created in order to control the new ACL engine behavior
- **BREAKING**: the *Burp1* and *Burp2* configuration sections have been merged into one single *Burp* section
- **BREAKING**: the *running* backups are now displayed in ``green`` instead of ``blue``
- **BREAKING**: the docker postgresql image was upgraded from 9.6 to 10.1, you'll have to manually upgrade/migrate your data `following this documentation <https://github.com/tianon/docker-postgres-upgrade>`_
- **BREAKING**: the ``docker-compose.yml`` file now uses the ``version: '2'`` format
- **BREAKING**: the old config file format with colons (:) as separator is no
- Add: new plugins system to allow users to write their own modules
- Add: `Italian translation <https://git.ziirish.me/ziirish/burp-ui/merge_requests/74>`_ thanks to Enrico
- Add: new `client configuration templates <https://git.ziirish.me/ziirish/burp-ui/issues/155>`_
- Add: `backups deletion <https://git.ziirish.me/ziirish/burp-ui/issues/203>`_
- Add: `show last client status in client view <https://git.ziirish.me/ziirish/burp-ui/issues/212>`_
- Add: `record login failure attempt <https://git.ziirish.me/ziirish/burp-ui/issues/214>`_
- Add: `support new burp counters <https://git.ziirish.me/ziirish/burp-ui/issues/219>`_
- Add: `support new burp pair options <https://git.ziirish.me/ziirish/burp-ui/issues/220>`_
- Add: `support new reset list (:=) syntax <https://git.ziirish.me/ziirish/burp-ui/issues/223>`_
- Add: `new websocket server <https://git.ziirish.me/ziirish/burp-ui/issues/224>`_
- Add: `new Administration panel <https://git.ziirish.me/ziirish/burp-ui/issues/222>`_
- Improvement: `better ACL engine <https://git.ziirish.me/ziirish/burp-ui/issues/221>`_
- Fix: issue `#213 <https://git.ziirish.me/ziirish/burp-ui/issues/213>`_
- Fix: issue `#225 <https://git.ziirish.me/ziirish/burp-ui/issues/225>`_
- Fix: issue `#226 <https://git.ziirish.me/ziirish/burp-ui/issues/226>`_
- Fix: issue `#227 <https://git.ziirish.me/ziirish/burp-ui/issues/227>`_
- Fix: issue `#234 <https://git.ziirish.me/ziirish/burp-ui/issues/234>`_
- Fix: issue `#235 <https://git.ziirish.me/ziirish/burp-ui/issues/235>`_
- Fix: issue `#236 <https://git.ziirish.me/ziirish/burp-ui/issues/236>`_
- Fix: issue `#242 <https://git.ziirish.me/ziirish/burp-ui/issues/242>`_
- Fix: issue `#245 <https://git.ziirish.me/ziirish/burp-ui/issues/245>`_
- Fix: issue `#246 <https://git.ziirish.me/ziirish/burp-ui/issues/246>`_
- Fix: issue `#247 <https://git.ziirish.me/ziirish/burp-ui/issues/247>`_
- Fix: issue `#248 <https://git.ziirish.me/ziirish/burp-ui/issues/248>`_
- Fix: issue `#251 <https://git.ziirish.me/ziirish/burp-ui/issues/251>`_
- Fix: issue `#257 <https://git.ziirish.me/ziirish/burp-ui/issues/257>`_
- Fix: issue `#262 <https://git.ziirish.me/ziirish/burp-ui/issues/262>`_
- Fix: issue `#263 <https://git.ziirish.me/ziirish/burp-ui/issues/263>`_
- Fix: issue `#264 <https://git.ziirish.me/ziirish/burp-ui/issues/264>`_
- `Full changelog <https://git.ziirish.me/ziirish/burp-ui/compare/0.5.0...0.6.0>`__
0.5.1 (05/26/2017)
------------------
- Fix: handle non ascii chars in the browser view
- Fix: issue `#215 <https://git.ziirish.me/ziirish/burp-ui/issues/215>`_
- Fix: issue `#218 <https://git.ziirish.me/ziirish/burp-ui/issues/218>`_
0.5.0 (05/09/2017)
------------------
- **BREAKING**: the *standalone* option has been renamed to *single* for less confusion
......
......@@ -4,6 +4,7 @@ Sorted by surname (or nickname).
bedaes
Diego Daguerre
Enrico
Pablo Estigarribia
Wade Fitzpatrick
Nigel Hathaway
......
BSD 3-clause LICENSE
BSD 3-clause License
The following License only applies to the burp-ui sources
================================================================================
Copyright (c) 2014-2018 by Benjamin SANS (Ziirish) <hi+burpui@ziirish.me> http://ziirish.info/
All rights reserved.
Copyright (c) 2014-2017 by Benjamin SANS (Ziirish) <hi+burpui@ziirish.me>
http://ziirish.info/
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Some rights reserved.
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
Redistribution and use in source and binary forms of the software as well
as documentation, with or without modification, are permitted provided
that the following conditions are met:
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* The names of the contributors may not be used to endorse or
promote products derived from this software without specific
prior written permission.
THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
================================================================================
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
......@@ -22,12 +22,13 @@ clean:
@find . -type d -name "__pycache__" -exec rm -rf "{}" \; || true
@find . -type f -name "*.pyc" -delete || true
@rm -rf build dist burp_ui.egg-info docs/_build || true
@cd docs && make clean
clean_coverage:
@rm -f .coverage
flake8:
@echo 'Checking pep8 compliance and errors...'
@flake8 --ignore=E501 burpui
@flake8 --ignore=E501,E722 burpui
check: pep8 pyflakes doc_coverage test
......@@ -35,6 +35,7 @@ You can now play with ``Burp-UI`` at
Credentials:
- *admin* / *admin* to play with ``Burp-UI`` as an administrator
- *moderator* / *moderator* to play with ``Burp-UI`` as a moderator
- *demo* / *demo* to play with ``Burp-UI`` as a regular user
What's that?
......@@ -97,6 +98,11 @@ deployments through Ansible:
- `burpui_server <https://galaxy.ansible.com/CoffeeITWorks/burpui_server/>`_
- `burp2_server <https://galaxy.ansible.com/CoffeeITWorks/burp2_server/>`_
@qm2k contributed some scripts/config to tweak your setup. You can found them here:
- `burp-ui_integration <https://github.com/qm2k/burp-ui_integration>`_
- `burp_integration <https://github.com/qm2k/burp_integration>`_
Licenses
--------
......@@ -118,7 +124,7 @@ But this project is built on top of other tools. Here is a non exhaustive list:
- `AngularStrap <http://mgcrea.github.io/angular-strap/>`_
- `lodash <https://github.com/lodash/lodash>`_
- `DataTables <http://datatables.net/>`_
- Home-made `favicon <https://git.ziirish.me/ziirish/burp-ui/blob/stable/burpui/static/images/favicon.ico>`_ based on pictures from `simpsoncrazy <http://www.simpsoncrazy.com/pictures/homer>`_
- Home-made `favicon <https://git.ziirish.me/ziirish/burp-ui/blob/master/burpui/static/images/favicon.ico>`_ based on pictures from `simpsoncrazy <http://www.simpsoncrazy.com/pictures/homer>`_
Also note that this project is made with the Awesome `Flask`_ micro-framework.
......@@ -135,7 +141,7 @@ would not exist without `Burp`_.
.. _Flask: http://flask.pocoo.org/
.. _License: https://git.ziirish.me/ziirish/burp-ui/blob/stable/LICENSE
.. _License: https://git.ziirish.me/ziirish/burp-ui/blob/master/LICENSE
.. _Burp: http://burp.grke.org/
.. _burpui.cfg: https://git.ziirish.me/ziirish/burp-ui/blob/stable/share/burpui/etc/burpui.sample.cfg
.. _burp-ui.readthedocs.io: https://burp-ui.readthedocs.io/en/stable/
......
......@@ -141,6 +141,7 @@ def celery():
parser = ArgumentParser('bui-celery')
parser.add_argument('-c', '--config', dest='config', help='burp-ui configuration file', metavar='<CONFIG>')
parser.add_argument('-t', '--type', dest='type', help='celery mode', metavar='<worker|beat|flower>')
parser.add_argument('-m', '--mode', dest='mode', help='application mode', metavar='<agent|server|worker|manage|legacy>')
parser.add_argument('remaining', nargs=REMAINDER)
......@@ -155,6 +156,11 @@ def celery():
else:
conf = lookup_file()
if options.type:
celery_mode = options.type
else:
celery_mode = 'worker'
# make conf path absolute
if not conf.startswith('/'):
curr = os.getcwd()
......@@ -169,7 +175,7 @@ def celery():
args = [
'celery',
'worker',
celery_mode,
'-A',
'worker.celery'
]
......@@ -183,6 +189,7 @@ def manage():
from burpui.utils import lookup_file
parser = ArgumentParser('bui-manage')
parser.add_argument('-v', '--verbose', dest='log', help='increase output verbosity (e.g., -vv is more verbose than -v)', action='count')
parser.add_argument('-c', '--config', dest='config', help='burp-ui configuration file', metavar='<CONFIG>')
parser.add_argument('-i', '--migrations', dest='migrations', help='migrations directory', metavar='<MIGRATIONSDIR>')
parser.add_argument('-m', '--mode', dest='mode', help='application mode', metavar='<agent|server|worker|manage|legacy>')
......@@ -210,9 +217,10 @@ def manage():
env['BUI_MODE'] = 'manage'
env['BUI_CONFIG'] = conf
env['BUI_VERBOSE'] = str(options.log)
if migrations:
env['BUI_MIGRATIONS'] = migrations
if os.path.isdir('burpui'):
if os.path.isdir('burpui') and os.path.isfile('burpui/cli.py'):
env['FLASK_APP'] = 'burpui/cli.py'
else:
env['FLASK_APP'] = 'burpui.cli'
......
......@@ -17,7 +17,7 @@ except ImportError:
if sys.version_info[0] >= 3:
PY3 = True
from urllib.parse import unquote, quote # noqa
from urllib.parse import unquote, quote, urlparse, urljoin # noqa
text_type = str
string_types = (str,)
......@@ -39,6 +39,7 @@ if sys.version_info[0] >= 3:
else:
PY3 = False
from urllib import unquote, quote # noqa
from urlparse import urlparse, urljoin # noqa
text_type = unicode
string_types = (str, unicode)
......@@ -62,7 +63,7 @@ def to_bytes(text):
"""Transform string to bytes."""
if isinstance(text, text_type):
text = text.encode('utf-8')
return text
return text or b''
def to_unicode(input_bytes, encoding='utf-8'):
......@@ -71,7 +72,7 @@ def to_unicode(input_bytes, encoding='utf-8'):
input_bytes = input_bytes.decode(encoding)
elif re.match(r'\\u[0-9a-f]{4}', input_bytes):
input_bytes = input_bytes.decode('unicode-escape')
return input_bytes
return input_bytes or u''
# maps module name -> attribute name -> original item
......
......@@ -28,6 +28,12 @@ from .utils import BUIlogging
from .config import config
from .desc import __version__
try:
from sendfile import sendfile
USE_SENDFILE = True
except ImportError:
USE_SENDFILE = False
G_PORT = 10000
G_BIND = u'::'
G_SSL = False
......@@ -219,11 +225,20 @@ class BUIAgent(BUIbackend, BUIlogging):
self.request.sendall(b'OK')
self.request.sendall(struct.pack('!Q', size))
with open(path, 'rb') as f:
buf = f.read(1024)
while buf:
self._logger('info', 'sending {} Bytes'.format(len(buf)))
self.request.sendall(buf)
buf = f.read(1024)
if not USE_SENDFILE:
while True:
buf = f.read(1024)
if not buf:
break
self._logger('info', 'sending {} Bytes'.format(len(buf)))
self.request.sendall(buf)
else:
offset = 0
while True:
sent = sendfile(self.request.fileno(), f.fileno(), offset, size)
if sent == 0:
break
offset += sent
os.unlink(path)
lengthbuf = self.request.recv(8)
length, = struct.unpack('!Q', lengthbuf)
......
......@@ -14,7 +14,7 @@ import uuid
import hashlib
import logging
from flask import Blueprint, Response, request, current_app, session
from flask import Blueprint, Response, request, current_app, session, abort
from flask_restplus import Api as ApiPlus
from flask_login import current_user
from importlib import import_module
......@@ -22,21 +22,26 @@ from functools import wraps
from .custom.namespace import Namespace
from .._compat import to_bytes
from ..desc import __version__, __release__, __url__, __doc__
from ..server import BUIServer # noqa
from ..exceptions import BUIserverException
from ..config import config
from ..ext.cache import cache
bui = current_app # type: BUIServer
EXEMPT_METHODS = set(['OPTIONS'])
def force_refresh():
return request.headers.get('X-No-Cache', False) is not False
def cache_key():
key = '{}-{}-{}-{}-{}'.format(
key = '{}-{}-{}-{}-{}-{}'.format(
session.get('login', uuid.uuid4()),
request.path,
request.values,
request.headers.get('X-Session-Tag', ''),
request.cookies,
session.get('language', '')
)
key = hashlib.sha256(to_bytes(key)).hexdigest()
......@@ -61,7 +66,7 @@ def api_login_required(func):
not bui.config.get('LOGIN_DISABLED', False)):
if not current_user.is_authenticated:
if request.headers.get('X-From-UI', False):
return Response('Access denied', 403)
abort(403)
return Response(
'Could not verify your access level for that URL.\n'
'You have to login with proper credentials', 401,
......@@ -70,21 +75,42 @@ def api_login_required(func):
return decorated_view
def check_acl(func):
"""Custom decorator to check if the ACL are in use or not"""
@wraps(func)
def decorated_view(*args, **kwargs):
if request.method in EXEMPT_METHODS: # pragma: no cover
return func(*args, **kwargs)
# 'func' is a Flask.view.MethodView so we have access to some special
# params
cls = func.view_class
login_required = getattr(cls, 'login_required', True)
if (bui.auth != 'none' and
login_required and
not bui.config.get('LOGIN_DISABLED', False)):
if current_user.is_anonymous:
abort(403)
return func(*args, **kwargs)
return decorated_view
class Api(ApiPlus):
"""Wrapper class around :class:`flask_restplus.Api`"""
logger = logging.getLogger('burp-ui')
# TODO: should use global object instead of reference
cache = cache
loaded = False
release = None
__doc__ = None
__url__ = None
release = __release__
__doc__ = __doc__
__url__ = __url__
CELERY_REQUIRED = ['async']
def load_all(self):
if config['WITH_LIMIT']:
from ..ext.limit import limiter
self.decorators.append(limiter.limit(config['BUI_RATIO']))
try:
from ..ext.limit import limiter
self.decorators.append(limiter.limit(config['BUI_RATIO']))
except ImportError:
self.logger.warning('Unable to import limiter module')
"""hack to automatically import api modules"""
if not self.loaded:
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
......@@ -117,7 +143,20 @@ class Api(ApiPlus):
def decorator(func):
@wraps(func)
def decorated(resource, *args, **kwargs):
if not resource.is_admin:
if not current_user.is_anonymous and \
not current_user.acl.is_admin():
resource.abort(code, message)
return func(resource, *args, **kwargs)
return decorated
return decorator
def acl_admin_or_moderator_required(self, message='Access denied', code=403):
def decorator(func):
@wraps(func)
def decorated(resource, *args, **kwargs):
if not current_user.is_anonymous and \
not current_user.acl.is_admin() and \
not current_user.acl.is_moderator():
resource.abort(code, message)
return func(resource, *args, **kwargs)
return decorated
......@@ -129,7 +168,9 @@ class Api(ApiPlus):
def decorated(resource, *args, **kwargs):
if key not in kwargs: # pragma: no cover
resource.abort(500, "key '{}' not found".format(key))
if kwargs[key] != resource.username and not resource.is_admin:
if kwargs[key] != current_user.name and \
not current_user.is_anonymous and \
not current_user.acl.is_admin():
resource.abort(code, message)
return func(resource, *args, **kwargs)
return decorated
......@@ -163,6 +204,7 @@ api = Api(
apibp,
title='Burp-UI API',
description='Burp-UI API to interact with burp',
version=__version__,
doc='/doc',
decorators=[api_login_required]
)
......
This diff is collapsed.
This diff is collapsed.
......@@ -13,6 +13,7 @@ from .custom import Resource
from ..exceptions import BUIserverException
from flask import current_app
from flask_login import current_user
bui = current_app # type: BUIServer
ns = api.namespace('backup', 'Backup methods')
......@@ -63,11 +64,9 @@ class ServerBackup(Resource):
if not name:
self.abort(400, 'Missing options')
# Manage ACL
if (bui.acl and
(not bui.acl.is_client_allowed(self.username,
name,
server) and not
self.is_admin)):
if not current_user.is_anonymous and \
not current_user.acl.is_admin() and \
not current_user.acl.is_client_allowed(name, server):
self.abort(403, 'You are not allowed to access this client')
try:
return {'is_server_backup': bui.client.is_server_backup(name, server)}
......@@ -99,11 +98,10 @@ class ServerBackup(Resource):
if not name:
self.abort(400, 'Missing options')
# Manage ACL
if (bui.acl and
(not bui.acl.is_client_allowed(self.username,
name,
server) and not
self.is_admin)):
if not current_user.is_anonymous and \
not current_user.acl.is_admin() and \
not current_user.acl.is_moderator() and \
not current_user.acl.is_client_rw(name, server):
self.abort(403, 'You are not allowed to cancel a backup for this client')
try:
return bui.client.cancel_server_backup(name, server)
......@@ -137,11 +135,10 @@ class ServerBackup(Resource):
if not name:
self.abort(400, 'Missing options')
# Manage ACL
if (bui.acl and
(not bui.acl.is_client_allowed(self.username,
name,
server) and not
self.is_admin)):
if not current_user.is_anonymous and \
not current_user.acl.is_admin() and \
not current_user.acl.is_moderator() and \
not current_user.acl.is_client_rw(name, server):
self.abort(
403,
'You are not allowed to schedule a backup for this client'
......
This diff is collapsed.
This diff is collapsed.
......@@ -13,8 +13,6 @@ import json
from flask_restplus import Resource as ResourcePlus
from flask_restplus.errors import abort
from flask_login import current_user
from flask import current_app as bui
from ..._compat import PY3
if PY3:
......@@ -29,17 +27,6 @@ class Resource(ResourcePlus):
logger = logging.getLogger('burp-ui')
def __init__(self, api=None, *args, **kwargs):
"""Constructor"""
curr = current_user._get_current_object()
self.username = getattr(curr, 'name', None)
# if there is no ACL, assume everybody is admin
self.is_admin = True
if bui.acl:
self.is_admin = bui.acl.is_admin(self.username)
self.cache = api.cache
ResourcePlus.__init__(self, api, *args, **kwargs)
def abort(self, code=500, message=None, **kwargs):
"""
Properly abort the current request
......
This diff is collapsed.
......@@ -61,14 +61,14 @@ class PrefsUI(Resource):
if bui.config['WITH_SQL'] and not bui.config['BUI_DEMO']:
from ..ext.sql import db
from ..models import Pref
pref = Pref.query.filter_by(user=self.username, key=key).first()
pref = Pref.query.filter_by(user=current_user.name, key=key).first()
if pref:
if val:
pref.value = val
else:
db.session.delete(pref)
elif val:
pref = Pref(self.username, key, val)
pref = Pref(current_user.name, key, val)
db.session.add(pref)
try:
db.session.commit()
......@@ -159,7 +159,7 @@ class PrefsUI(Resource):
from ..models import Pref
try:
Pref.query.filter_by(
user=self.username,
user=current_user.name,
key=key
).delete()
db.session.commit()
......
......@@ -20,6 +20,7 @@ from zlib import adler32
from time import gmtime, strftime, time
from flask import Response, send_file, make_response, after_this_request, \
current_app
from flask_login import current_user
from werkzeug.datastructures import Headers
from werkzeug.exceptions import HTTPException
......@@ -84,20 +85,18 @@ class Restore(Resource):
:returns: A :mod:`flask.Response` object representing an archive of the restored files
"""
args = self.parser.parse_args()
l = args['list']
s = args['strip']
f = args['format'] or 'zip'
p = args['pass']
lst = args['list']
stp = args['strip']
fmt = args['format'] or 'zip'
pwd = args['pass']
resp = None
# Check params
if not l or not name or not backup:
if not lst or not name or not backup:
self.abort(400, 'missing arguments')
# Manage ACL
if (bui.acl and
(not bui.acl.is_client_allowed(self.username,
name,
server) and not
self.is_admin)):
if not current_user.is_anonymous and \
not current_user.acl.is_admin() and \
not current_user.acl.is_client_rw(name, server):
self.abort(403, 'You are not allowed to perform a restoration for this client')
if server:
filename = 'restoration_%d_%s_on_%s_at_%s.%s' % (
......@@ -105,19 +104,25 @@ class Restore(Resource):
name,
server,
strftime("%Y-%m-%d_%H_%M_%S", gmtime()),
f)
fmt)
else:
filename = 'restoration_%d_%s_at_%s.%s' % (
backup,
name,
strftime("%Y-%m-%d_%H_%M_%S", gmtime()),
f)
fmt)
archive, err = bui.client.restore_files(name, backup, l, s, f, p, server)
archive, err = bui.client.restore_files(name, backup, lst, stp, fmt, pwd, server)
if not archive:
if err:
if (not current_user.is_anonymous and
not current_user.acl.is_admin() or
bui.demo) and err != 'encrypted':
err = 'An error occurred while performing the ' \
'restoration. Please contact your administrator ' \
'for further details'
return make_response(err, 500)
self.abort(500)
return make_response(err, 500)
if not server:
try:
......@@ -127,7 +132,7 @@ class Restore(Resource):
# ended. Because the fh is open, the file will be actually removed
# when the transfer is done and the send_file method has closed
# the fh. Only tested on Linux systems.
fh = open(archive, 'r')
fh = open(archive, 'rb')
@after_this_request
def remove_file(response):
......@@ -143,9 +148,9 @@ class Restore(Resource):
attachment_filename=filename,
mimetype='application/zip')
resp.set_cookie('fileDownload', 'true')
except Exception as e:
bui.client.logger.error(str(e))
self.abort(500, str(e))
except Exception as exp:
bui.client.logger.error(str(exp))
self.abort(500, str(exp))
else:
# Multi-agent mode
try:
......@@ -158,25 +163,25 @@ class Restore(Resource):
bui.client.logger.debug('Need to get {} Bytes : {}'.format(length, socket))
def stream_file(sock, l):
def stream_file(sock, size):
"""The restoration took place on another server so we need
to stream the file that is not present on the current
machine.
"""
bsize = 1024
received = 0
if l < bsize:
bsize = l
while received < l:
if size < bsize:
bsize = size
while received < size:
buf = b''
r, _, _ = select.select([sock], [], [], 5)
if not r:
read, _, _ = select.select([sock], [], [], 5)
if not read:
raise Exception('Socket timed-out')
buf += sock.recv(bsize)
if not buf:
continue
received += len(buf)
self.logger.debug('{}/{}'.format(received, l))
self.logger.debug('{}/{}'.format(received, size))
yield buf
sock.sendall(struct.pack('!Q', 2))
sock.sendall(b'RE')
......@@ -197,11 +202,11 @@ class Restore(Resource):
time(),
length,
adler32(filename.encode('utf-8')) & 0xffffffff))
except HTTPException as e:
raise e
except Exception as e:
bui.client.logger.error(str(e))
self.abort(500, str(e))
except HTTPException as exp:
raise exp
except Exception as exp:
bui.client.logger.error(str(exp))
self.abort(500, str(exp))
return resp
......@@ -297,11 +302,9 @@ class ServerRestore(Resource):
if not name:
self.abort(400, 'Missing options')
# Manage ACL
if (bui.acl and
(not bui.acl.is_client_allowed(self.username,
name,
server) and not
self.is_admin)):
if not current_user.is_anonymous and \
not current_user.acl.is_admin() and \
not current_user.acl.is_client_rw(name, server):
self.abort(403, 'You are not allowed to edit a restoration for this client')
try:
return bui.client.is_server_restore(name, server)
......@@ -333,11 +336,9 @@ class ServerRestore(Resource):
if not name:
self.abort(400, 'Missing options')
# Manage ACL
if (bui.acl and
(not bui.acl.is_client_allowed(self.username,
name,
server) and not
self.is_admin)):
if not current_user.is_anonymous and \
not current_user.acl.is_admin() and \
not current_user.acl.is_client_rw(name, server):
self.abort(403, 'You are not allowed to cancel a restoration for this client')
try:
return bui.client.cancel_server_restore(name, server)
......@@ -407,11 +408,11 @@ class DoServerRestore(Resource):
strip = args['strip-sc']
prefix = args['prefix-sc']
force = args['force-sc']
to = args['restoreto-sc']
to = args['restoreto-sc'] or name
json = []
if not bui.client.get_parser(agent=server).param('server_can_restore',
'client_conf') or \
'client_conf') and \
bui.noserverrestore:
self.abort(
428,
......@@ -423,20 +424,17 @@ class DoServerRestore(Resource):
if not files_list or not name or not backup:
self.abort(400, 'Missing options')
# Manage ACL
if (bui.acl and
(not bui.acl.is_client_allowed(self.username,
name,
server) and not
self.is_admin and
(to and not
bui.acl.is_client_allowed(self.username,
to,
server)))):
if not current_user.is_anonymous and \
not current_user.acl.is_admin() and \
not current_user.acl.is_client_rw(to, server) and \
not current_user.acl.is_client_allowed(to, server):
self.abort(
403,
'You are not allowed to perform a restoration for this client'
)
try:
if to == name:
to = None
json = bui.client.server_restore(
name,
backup,
......
# -*- coding: utf8 -*-
# This is a submodule we can also use "from ..api import api"
from . import api, cache_key
from . import api, cache_key, force_refresh
from ..server import BUIServer # noqa
from .custom import fields, Resource
from ..ext.cache import cache
from ..decorators import browser_cache
from ..exceptions import BUIserverException
from flask import current_app
from flask_login import current_user
from six import iteritems
bui = current_app # type: BUIServer
......@@ -26,13 +29,14 @@ class ServersStats(Resource):
'name': fields.String(required=True, description='Server name'),
})
@api.cache.cached(timeout=1800, key_prefix=cache_key)
@cache.cached(timeout=1800, key_prefix=cache_key, unless=force_refresh)
@ns.marshal_list_with(servers_fields, code=200, description='Success')
@ns.doc(
responses={
500: 'Internal failure',
},
)
@browser_cache(1800)
def get(self):
"""Returns a list of servers (agents) with basic stats
......@@ -59,15 +63,13 @@ class ServersStats(Resource):
"""
r = []
restrict = []
check = False
if bui.standalone:
return r
if bui.acl and not self.is_admin:
if not current_user.is_anonymous and not current_user.acl.is_admin():
check = True
restrict = bui.acl.servers(self.username)
for serv in bui.client.servers:
try:
......@@ -75,18 +77,19 @@ class ServersStats(Resource):
except BUIserverException:
alive = False
if check and serv in restrict:
try:
clients = bui.client.servers[serv].get_all_clients(serv)
except BUIserverException:
clients = []
if check and current_user.acl.is_server_allowed(serv):
allowed_clients = [x for x in clients if current_user.acl.is_client_allowed(x['name'], serv)]
r.append({
'name': serv,
'clients': len(bui.acl.clients(self.username, serv)),
'clients': len(allowed_clients),
'alive': alive
})
elif not check:
try:
clients = bui.client.servers[serv].get_all_clients(serv)
except BUIserverException:
clients = []
r.append({
'name': serv,
'clients': len(clients),
......@@ -125,7 +128,7 @@ class ServersReport(Resource):
'servers': fields.Nested(server_fields, as_list=True, required=True),
})
@api.cache.cached(timeout=1800, key_prefix=cache_key)
@cache.cached(timeout=1800, key_prefix=cache_key, unless=force_refresh)
@ns.marshal_with(report_fields, code=200, description='Success')
@ns.doc(
responses={
......@@ -133,6 +136,7 @@ class ServersReport(Resource):
500: 'Internal failure',
},
)
@browser_cache(1800)
def get(self):
"""Returns a global report about all the servers managed by Burp-UI
......@@ -168,11 +172,9 @@ class ServersReport(Resource):
:returns: The *JSON* described above.
"""
r = {}
restrict = []
check = False
if bui.acl and not self.is_admin:
if not current_user.is_anonymous and not current_user.acl.is_admin():
check = True
restrict = bui.acl.servers(self.username)
backups = []
servers = []
......@@ -187,13 +189,11 @@ class ServersReport(Resource):
},
'number': 0
}
if check and serv not in restrict:
if check and not current_user.acl.is_server_allowed(serv):
continue
clients = []
if bui.acl and not self.is_admin:
clients = [{'name': x} for x in bui.acl.clients(self.username, serv)]
else:
clients = bui.client.get_all_clients(agent=serv)
clients = bui.client.get_all_clients(agent=serv)
if check:
clients = [x for x in clients if current_user.acl.is_client_allowed(x['name'], serv)]
j = bui.client.get_clients_report(clients, serv)
if 'clients' not in j or 'backups' not in j:
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -9,22 +9,15 @@
"""
import os
import re
import json
import codecs
import shutil
import datetime
import logging
import configobj
import validate
from .desc import __version__, __release__
class BUIConfig(dict):
"""Custom config parser"""
logger = logging.getLogger('burp-ui')
delta = datetime.timedelta(seconds=30)
last = datetime.datetime.now() - delta
mtime = 0
def __init__(self, config=None, explain=False, defaults=None):
......@@ -61,36 +54,19 @@ class BUIConfig(dict):
self.validator = validate.Validator()
try:
self.conf = configobj.ConfigObj(config, encoding='utf-8')
self.last = datetime.datetime.now()
self.mtime = os.path.getmtime(self.conffile)
except configobj.ConfigObjError as exp:
# We were unable to parse the config, maybe we need to
# convert/update it
self.logger.warning(
'Unable to parse the configuration... Trying to convert it'
)
# if conversion is successful, give it another try
if self._convert(config):
# This time, if it fails, the exception will be forwarded
try:
self.conf = configobj.ConfigObj(config)
except configobj.ConfigObjError as exp2:
if explain:
self._explain(exp2)
else:
raise exp2
# We were unable to parse the config
self.logger.critical('Unable to convert configuration')
if explain:
self._explain(exp)
else:
self.logger.critical('Unable to convert configuration')
if explain:
self._explain(exp)
else:
raise exp
raise exp
@property
def options(self):
"""ConfigObj object"""
if (datetime.datetime.now() - self.last) > self.delta:
self._refresh()
self._refresh()
return self.conf
@property
......@@ -130,7 +106,7 @@ class BUIConfig(dict):
conf.
"""
ret = True
if section not in self.options:
if not self.section_exists(section):
# look for the section in the comments
conffile = self.options.filename
source = source or conffile
......@@ -154,15 +130,38 @@ class BUIConfig(dict):
ret = False
return ret
def section_exists(self, section):
"""Check whether a section exists or not"""
return section in self.options
def rename_section(self, old_section, new_section, source=None):
"""Rename a given section"""
ret = False
if not self.section_exists(old_section):
return ret
conffile = self.options.filename
source = source or conffile
ori = []
with codecs.open(source, 'r', 'utf-8', errors='ignore') as config:
ori = [x.rstrip('\n') for x in config.readlines()]
if ori:
with codecs.open(conffile, 'w', 'utf-8', errors='ignore') as config:
for line in ori:
if re.match(r'^\s*(#|;)+\s*\[{}\]'.format(old_section), line):
config.write('{}\n'.format(line.replace(old_section, new_section)))
ret = True
else:
config.write('{}\n'.format(line))
return ret
def changed(self, id):
"""Check if the conf has changed"""
if (datetime.datetime.now() - self.last) > self.delta:
self._refresh()
# don't use delta for cases where we run several gunicorn workers
self._refresh()
return id != self.mtime
def _refresh(self, force=False):
"""Refresh conf"""
self.last = datetime.datetime.now()
mtime = os.path.getmtime(self.conffile)
if mtime != self.mtime or force:
self.logger.debug('Configuration changed')
......@@ -177,62 +176,6 @@ class BUIConfig(dict):
"""Set the default section"""
self.section = section
def _convert(self, config):
"""Convert an old config to a new one"""
sav = '{}.back'.format(config)
current_section = None
if os.path.exists(sav):
self.logger.error(
'Looks like the configuration file has already been converted'
)
return False
try:
shutil.copy(config, sav)
except IOError as exp:
self.logger.error(str(exp))
return False