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'
......
......@@ -8,17 +8,21 @@
"""
import os
import re
from . import api, cache_key
from . import api, cache_key, force_refresh
from ..server import BUIServer # noqa
from .custom import fields, Resource
from .custom.inputs import boolean
from ..decorators import browser_cache
from ..ext.cache import cache
from ..exceptions import BUIserverException
from ..utils import NOTIF_ERROR
from six import iteritems
from flask_restplus.marshalling import marshal
from flask import current_app
from flask import current_app, request
from flask_login import current_user
bui = current_app # type: BUIServer
ns = api.namespace('client', 'Client methods')
......@@ -152,8 +156,16 @@ class ClientTree(Resource):
required=False,
default=False
)
parser.add_argument(
'init',
type=boolean,
help='First call to load the root of the tree',
nullable=True,
required=False,
default=False
)
@api.cache.cached(timeout=3600, key_prefix=cache_key)
@cache.cached(timeout=3600, key_prefix=cache_key, unless=force_refresh)
@ns.marshal_list_with(node_fields, code=200, description='Success')
@ns.expect(parser)
@ns.doc(
......@@ -162,6 +174,7 @@ class ClientTree(Resource):
'500': 'Internal failure',
},
)
@browser_cache(3600)
def get(self, server=None, name=None, backup=None):
"""Returns a list of 'nodes' under a given path
......@@ -217,13 +230,25 @@ class ClientTree(Resource):
paths_loaded = []
to_select_list = []
if (bui.acl and
(not self.is_admin and not
bui.acl.is_client_allowed(self.username,
name,
server))):
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, 'Sorry, you are not allowed to view this client')
from_cookie = None
if args['init'] and not root_list:
from_cookie = request.cookies.get('fancytree-1-expanded', '')
if from_cookie:
args['recursive'] = True
_root = bui.client.get_tree(name, backup, agent=server)
root_list = [x['name'] for x in _root]
for path in from_cookie.split('~'):
if not path.endswith('/'):
path += '/'
if path not in root_list:
root_list.append(path)
root_list = sorted(root_list)
try:
root_list_clean = []
......@@ -357,7 +382,7 @@ class ClientTreeAll(Resource):
help='Which server to collect data from when in multi-agent mode'
)
@api.cache.cached(timeout=3600, key_prefix=cache_key)
@cache.cached(timeout=3600, key_prefix=cache_key, unless=force_refresh)
@ns.marshal_list_with(node_fields, code=200, description='Success')
@ns.expect(parser)
@ns.doc(
......@@ -367,6 +392,7 @@ class ClientTreeAll(Resource):
'500': 'Internal failure',
},
)
@browser_cache(3600)
def get(self, server=None, name=None, backup=None):
"""Returns a list of all 'nodes' of a given backup
......@@ -420,11 +446,9 @@ class ClientTreeAll(Resource):
'Sorry, the requested backend does not support this method'
)
if (bui.acl and
(not self.is_admin and not
bui.acl.is_client_allowed(self.username,
name,
server))):
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, 'Sorry, you are not allowed to view this client')
try:
......@@ -627,7 +651,7 @@ class ClientReport(Resource):
),
})
@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.expect(parser)
@ns.doc(
......@@ -636,6 +660,7 @@ class ClientReport(Resource):
'500': 'Internal failure',
},
)
@browser_cache(1800)
def get(self, server=None, name=None, backup=None):
"""Returns a global report of a given backup/client
......@@ -784,42 +809,87 @@ class ClientReport(Resource):
:returns: The *JSON* described above.
"""
server = server or self.parser.parse_args()['serverName']
j = []
json = []
if not name:
err = [[1, 'No client defined']]
self.abort(400, err)
if (bui.acl and not
bui.acl.is_client_allowed(self.username,
name,
server)):
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 don\'t have rights to view this client report')
if backup:
try:
j = bui.client.get_backup_logs(backup, name, agent=server)
except BUIserverException as e:
self.abort(500, str(e))
json = bui.client.get_backup_logs(backup, name, agent=server)
except BUIserverException as exp:
self.abort(500, str(exp))
else:
try:
cl = bui.client.get_client(name, agent=server)
except BUIserverException as e:
self.abort(500, str(e))
client = bui.client.get_client(name, agent=server)
except BUIserverException as exp:
self.abort(500, str(exp))
err = []
for c in cl:
for back in client:
try:
j.append(
json.append(
bui.client.get_backup_logs(
c['number'],
back['number'],
name,
agent=server
)
)
except BUIserverException as e:
temp = [NOTIF_ERROR, str(e)]
except BUIserverException as exp:
temp = [NOTIF_ERROR, str(exp)]
if temp not in err:
err.append(temp)
if err:
self.abort(500, err)
return j
return json
@api.disabled_on_demo()
@ns.marshal_with(report_fields, code=202, description='Success')
@ns.expect(parser)
@ns.doc(
responses={
'400': 'Missing arguments',
'403': 'Insufficient permissions',
'500': 'Internal failure',
},
)
def delete(self, name, backup, server=None):
"""Deletes a given backup from the server
**DELETE** method provided by the webservice.
The access is filtered by the :mod:`burpui.misc.acl` module so that you
can only delete backups you have access to.
:param server: Which server to collect data from when in multi-agent
mode
:type server: str
:param name: The client we are working on
:type name: str
:param backup: The backup we are working on
:type backup: int
"""
server = server or self.parser.parse_args()['serverName']
if not name:
err = [[1, 'No client defined']]
self.abort(400, err)
if not current_user.is_anonymous and \
not current_user.acl.is_admin() and \
(not current_user.acl.is_moderator() or
current_user.acl.is_moderator() and
not current_user.acl.is_client_rw(name, server)):
self.abort(403, 'You don\'t have rights on this client')
msg = bui.client.delete_backup(name, backup, server)
if msg:
self.abort(500, msg)
return 202, ''
@ns.route('/stats/<name>',
......@@ -865,7 +935,7 @@ class ClientStats(Resource):
),
})
@api.cache.cached(timeout=1800, key_prefix=cache_key)
@cache.cached(timeout=1800, key_prefix=cache_key, unless=force_refresh)
@ns.marshal_list_with(client_fields, code=200, description='Success')
@ns.expect(parser)
@ns.doc(
......@@ -874,6 +944,7 @@ class ClientStats(Resource):
'500': 'Internal failure',
},
)
@browser_cache(1800)
def get(self, server=None, name=None):
"""Returns a list of backups for a given client
......@@ -907,13 +978,203 @@ class ClientStats(Resource):
"""
server = server or self.parser.parse_args()['serverName']
try:
if (bui.acl and (
not self.is_admin and
not bui.acl.is_client_allowed(self.username,
name,
server))):
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, 'Sorry, you cannot access this client')
j = bui.client.get_client(name, agent=server)
except BUIserverException as e:
self.abort(500, str(e))
return j
json = bui.client.get_client(name, agent=server)
except BUIserverException as exp:
self.abort(500, str(exp))
return json
@ns.route('/labels/<name>',
'/<server>/labels/<name>',
endpoint='client_labels')
@ns.doc(
params={
'server': 'Which server to collect data from when in multi-agent' +
' mode',
'name': 'Client name',
},
)
class ClientLabels(Resource):
"""The :class:`burpui.api.client.ClientLabels` resource allows you to
retrieve the labels of a given client.
This resource is part of the :mod:`burpui.api.client` module.
An optional ``GET`` parameter called ``serverName`` is supported when
running in multi-agent mode.
"""
parser = ns.parser()
parser.add_argument(
'serverName',
help='Which server to collect data from when in multi-agent mode'
)
parser.add_argument('clientName', help='Client name')
labels_fields = ns.model('ClientLabels', {
'labels': fields.List(fields.String, description='List of labels'),
})
@cache.cached(timeout=1800, key_prefix=cache_key, unless=force_refresh)
@ns.marshal_list_with(labels_fields, code=200, description='Success')
@ns.expect(parser)
@ns.doc(
responses={
'403': 'Insufficient permissions',
'500': 'Internal failure',
},
)
@browser_cache(1800)
def get(self, server=None, name=None):
"""Returns the labels of a given client
**GET** method provided by the webservice.
The *JSON* returned is:
::
{
"labels": [
"label1",
"label2"
]
}
The output is filtered by the :mod:`burpui.misc.acl` module so that you
only see stats about the clients you are authorized to.
:param server: Which server to collect data from when in multi-agent
mode
:type server: str
:param name: The client we are working on
:type name: str
:returns: The *JSON* described above.
"""
try:
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, 'Sorry, you cannot access this client')
labels = self._get_labels(name, server)
except BUIserverException as exp:
self.abort(500, str(exp))
return {'labels': labels}
@staticmethod
def _get_labels(client, server):
key = 'labels-{}-{}'.format(client, server)
ret = cache.cache.get(key)
if ret is not None:
return ret
labels = bui.client.get_client_labels(client, agent=server)
ret = []
for label in labels:
if bui.ignore_labels and \
re.search('|'.join(bui.ignore_labels), label):
continue
tmp_label = label
if bui.format_labels:
for regex, replace in bui.format_labels:
tmp_label = re.sub(regex, replace, tmp_label)
ret.append(tmp_label)
cache.cache.set(key, ret, 1800)
return ret
@ns.route('/running',
'/running/<name>',
'/<server>/running',
'/<server>/running/<name>',
endpoint='client_running_status')
@ns.doc(
params={
'server': 'Which server to collect data from when in multi-agent' +
' mode',
'name': 'Client name',
},
)
class ClientRunningStatus(Resource):
"""The :class:`burpui.api.client.ClientRunningStatus` resource allows you to
retrieve the running status of a given client.
This resource is part of the :mod:`burpui.api.client` module.
An optional ``GET`` parameter called ``serverName`` is supported when
running in multi-agent mode.
"""
parser = ns.parser()
parser.add_argument(
'serverName',
help='Which server to collect data from when in multi-agent mode'
)
parser.add_argument('clientName', help='Client name')
running_fields = ns.model('ClientRunningStatus', {
'state': fields.LocalizedString(required=True, description='Running state'),
'percent': fields.Integer(
required=False,
description='Backup progress in percent',
default=-1
),
'phase': fields.String(
required=False,
description='Backup phase',
default=None
),
'last': fields.DateTime(
required=False,
dt_format='iso8601',
description='Date of last backup'
),
})
@ns.marshal_list_with(running_fields, code=200, description='Success')
@ns.expect(parser)
@ns.doc(
responses={
'403': 'Insufficient permissions',
'500': 'Internal failure',
},
)
def get(self, server=None, name=None):
"""Returns the running status of a given client
**GET** method provided by the webservice.
The *JSON* returned is:
::
{
"state": "running",
"percent": 42,
"phase": "2",
"last": "now"
}
The output is filtered by the :mod:`burpui.misc.acl` module so that you
only see stats about the clients you are authorized to.
:param server: Which server to collect data from when in multi-agent
mode
:type server: str
:param name: The client we are working on
:type name: str
:returns: The *JSON* described above.
"""
args = self.parser.parse_args()
server = server or args['serverName']
name = name or args['clientName']
try:
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, 'Sorry, you cannot access this client')
json = bui.client.get_client_status(name, agent=server)
except BUIserverException as exp:
self.abort(500, str(exp))
return json
......@@ -7,21 +7,22 @@
.. moduleauthor:: Ziirish <hi+burpui@ziirish.me>
"""
from . import api, cache_key
from . import api, cache_key, force_refresh
from ..server import BUIServer # noqa
from .custom import fields, Resource
from .client import ClientLabels
from ..ext.cache import cache
from ..exceptions import BUIserverException
from ..decorators import browser_cache
from six import iteritems
from flask import current_app
from flask_login import current_user
bui = current_app # type: BUIServer
ns = api.namespace('clients', 'Clients methods')
# Seem to not be used anymore
# TODO: we can probably remove this someday
@ns.route('/running',
'/<server>/running',
'/running/<client>',
......@@ -58,7 +59,7 @@ class RunningClients(Resource):
The output is filtered by the :mod:`burpui.misc.acl` module so that you
only see stats about the clients you are authorized to.
only see stats about the clients you are authorized to see.
:param server: Which server to collect data from when in multi-agent mode
:type server: str
......@@ -69,34 +70,50 @@ class RunningClients(Resource):
:returns: The *JSON* described above.
"""
server = server or self.parser.parse_args()['serverName']
return self._running_clients(None, client, server)
def _running_clients(self, res, client, server):
if client:
if bui.acl:
if (not self.is_admin and not
bui.acl.is_client_allowed(self.username,
client,
server)):
r = []
return r
if not current_user.is_anonymous and \
not current_user.acl.is_admin() and \
not current_user.acl.is_client_allowed(client, server):
return []
if bui.client.is_backup_running(client, server):
r = [client]
return r
return [client]
else:
r = []
return r
return []
r = bui.client.is_one_backup_running(server)
running = res or bui.client.is_one_backup_running(server)
# Manage ACL
if bui.acl and not self.is_admin:
if isinstance(r, dict):
new = {}
for serv in bui.acl.servers(self.username):
allowed = bui.acl.clients(self.username, serv)
new[serv] = [x for x in r[serv] if x in allowed]
r = new
if not current_user.is_anonymous and not current_user.acl.is_admin():
if isinstance(running, dict):
ret = {}
def __extract_running_clients(serv):
try:
clients = [x['name'] for x in bui.client.get_all_clients(serv)]
except BUIserverException:
clients = []
allowed = [x for x in clients if current_user.acl.is_client_allowed(x, serv)]
return [x for x in running[serv] if x in allowed]
if server:
return __extract_running_clients(server)
for serv in bui.client.servers:
ret[serv] = __extract_running_clients(serv)
return ret
else:
allowed = bui.acl.clients(self.username, server)
r = [x for x in r if x in allowed]
return r
try:
clients = [x['name'] for x in bui.client.get_all_clients(server)]
except BUIserverException:
clients = []
allowed = [x for x in clients if current_user.acl.is_client_allowed(x, server)]
running = [x for x in running if x in allowed]
elif server and isinstance(running, dict):
return running.get(server, [])
return running
@ns.route('/backup-running',
......@@ -118,7 +135,6 @@ class RunningBackup(Resource):
'running': fields.Boolean(required=True, description='Is there a backup running right now'),
})
@api.cache.cached(timeout=60, key_prefix=cache_key)
@ns.marshal_with(running_fields, code=200, description='Success')
def get(self, server=None):
"""Tells if a backup is running right now
......@@ -151,15 +167,23 @@ class RunningBackup(Resource):
def _is_one_backup_running(self, res, server):
"""Check if a backup is running"""
# Manage ACL
if bui.acl and not self.is_admin:
if not current_user.is_anonymous and not current_user.acl.is_admin():
if isinstance(res, dict):
new = {}
for serv in bui.acl.servers(self.username):
allowed = bui.acl.clients(self.username, serv)
for serv in bui.client.servers:
try:
clients = [x['name'] for x in bui.client.get_all_clients(serv)]
except BUIserverException:
clients = []
allowed = [x for x in clients if current_user.acl.is_client_allowed(x, serv)]
new[serv] = [x for x in res[serv] if x in allowed]
res = new
else:
allowed = bui.acl.clients(self.username, server)
try:
clients = [x['name'] for x in bui.client.get_all_clients(server)]
except BUIserverException:
clients = []
allowed = [x for x in clients if current_user.acl.is_client_allowed(x, server)]
res = [x for x in res if x in allowed]
running = False
if isinstance(res, dict):
......@@ -219,7 +243,7 @@ class ClientsReport(Resource):
'clients': fields.Nested(client_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.expect(parser)
@ns.doc(
......@@ -228,6 +252,7 @@ class ClientsReport(Resource):
500: 'Internal failure',
},
)
@browser_cache(1800)
def get(self, server=None):
"""Returns a global report about all the clients of a given server
......@@ -283,10 +308,9 @@ class ClientsReport(Resource):
def _check_acl(self, server):
# Manage ACL
if (not bui.standalone and bui.acl and
(not self.is_admin and
server not in
bui.acl.servers(self.username))):
if (not bui.standalone and not current_user.is_anonymous and
(not current_user.acl.is_admin() and
not current_user.acl.is_server_allowed(server))):
self.abort(403, 'Sorry, you don\'t have any rights on this server')
def _get_clients_reports(self, res=None, server=None):
......@@ -361,22 +385,20 @@ class ClientsReport(Resource):
def _parse_clients_reports(self, res=None, server=None):
if not res:
clients = []
if bui.acl and not self.is_admin:
clients = [{'name': x} for x in bui.acl.clients(self.username, server)]
else:
try:
clients = bui.client.get_all_clients(agent=server)
except BUIserverException as e:
self.abort(500, str(e))
try:
clients = bui.client.get_all_clients(agent=server)
except BUIserverException as e:
self.abort(500, str(e))
if not current_user.is_anonymous and not current_user.acl.is_admin():
clients = [x for x in clients if current_user.acl.is_client_allowed(x['name'], server)]