add: new static templates (fix #280)

This commit is contained in:
ziirish 2019-10-03 11:51:25 +02:00
parent 1a8fcd5da9
commit 2292ef826f
No known key found for this signature in database
GPG key ID: 72DB229A64B54E46
11 changed files with 477 additions and 36 deletions

View file

@ -13,6 +13,7 @@ Current
- Add: new `listen` and `listen_status` options in burp-2.2.10 `#279 <https://git.ziirish.me/ziirish/burp-ui/issues/279>`_
- Add: new `order` keyword in ACL definitions in order to decide whether `rw` should be evaluated first or not `#305 <https://git.ziirish.me/ziirish/burp-ui/issues/305>`__
- Add: new `exclude` keyword in ACL definitions in order to exclude some clients from the rules `#305 <https://git.ziirish.me/ziirish/burp-ui/issues/305>`__
- Add: new *static templates* that allow you to create *onetime* (variables) templates `#280 <https://git.ziirish.me/ziirish/burp-ui/issues/280>`_
- Add: allow to hide selected clients/servers `#282 <https://git.ziirish.me/ziirish/burp-ui/issues/282>`_
- Add: allow to delete clients data upon removal `#232 <https://git.ziirish.me/ziirish/burp-ui/issues/232>`_
- Add: allow to create clients from templates in one call `#266 <https://git.ziirish.me/ziirish/burp-ui/issues/266>`_

View file

@ -7,6 +7,8 @@
.. moduleauthor:: Ziirish <hi+burpui@ziirish.me>
"""
import json
from . import api
from ..engines.server import BUIServer # noqa
from ..ext.cache import cache
@ -15,11 +17,14 @@ from .._compat import unquote
from ..utils import NOTIF_INFO
from flask_babel import gettext as _, refresh
from flask import jsonify, request, url_for, current_app, g, session
from flask import jsonify, request, url_for, current_app, g, session, render_template_string
from flask_login import current_user
from flask_restplus import inputs
from jinja2 import Environment, meta
from ..datastructures import ImmutableMultiDict, MultiDict
TEMPLATE_EXCLUDES = ['client', 'agent']
bui = current_app # type: BUIServer
ns = api.namespace('settings', 'Settings methods')
@ -323,6 +328,89 @@ class ClientsList(Resource):
return jsonify(result=res)
@ns.route('/static-templates',
'/<server>/static-templates',
endpoint='static_templates_list')
@ns.doc(
params={
'server': 'Which server to collect data from when in multi-agent mode',
},
)
class StaticTemplatesList(Resource):
@api.acl_admin_or_moderator_required(message='Sorry, you don\'t have rights to access the setting panel')
@ns.doc(
responses={
200: 'Success',
403: 'Insufficient permissions',
500: 'Internal failure',
}
)
def get(self, server=None):
"""Returns a list of clients"""
parser = bui.client.get_parser(agent=server)
res = parser.list_static_templates()
env = Environment()
for obj in res:
ast = env.parse(obj['content'])
obj['variables'] = [x for x in meta.find_undeclared_variables(ast) if x not in TEMPLATE_EXCLUDES]
return jsonify(result=res)
@ns.route('/static-template',
'/<server>/static-template',
endpoint='new_static_template',
methods=['PUT'])
@ns.doc(
params={
'server': 'Which server to collect data from when in multi-agent mode',
},
)
class NewStaticTemplateSettings(Resource):
parser = ns.parser()
parser.add_argument('newstatictemplate', required=True, help="No 'newstatictemplate' provided")
@api.disabled_on_demo()
@api.acl_admin_or_moderator_required(message='Sorry, you don\'t have rights to access the setting panel')
@ns.expect(parser)
@ns.doc(
responses={
200: 'Success',
400: 'Missing parameter',
403: 'Insufficient permissions',
500: 'Internal failure',
}
)
def put(self, server=None):
"""Creates a new template"""
if not current_user.is_anonymous and \
current_user.acl.is_moderator() and \
not current_user.acl.is_server_rw(server):
self.abort(403, 'You don\'t have rights on this server')
newtemplate = self.parser.parse_args()['newstatictemplate']
if not newtemplate:
self.abort(400, 'No template name provided')
parser = bui.client.get_parser(agent=server)
templates = parser.list_static_templates()
if any(tpl['name'] == newtemplate for tpl in templates):
self.abort(409, "Static template '{}' already exists".format(newtemplate))
# clientconfdir = bui.client.get_parser_attr('clientconfdir', server)
# if not clientconfdir:
# flash('Could not proceed, no \'clientconfdir\' find', 'warning')
# return redirect(request.referrer)
noti = bui.client.store_conf_cli(ImmutableMultiDict(), newtemplate, None, False, True, server)
if server:
url = url_for('view.cli_settings', server=server, client=newtemplate, statictemplate=True)
else:
url = url_for('view.cli_settings', client=newtemplate, statictemplate=True)
noti.append([NOTIF_INFO, _('<a href="%(url)s">Click here</a> to edit \'%(template)s\' configuration', url=url, template=newtemplate)])
# clear the cache when we add a new client
cache.clear()
bui.audit.logger.info(f'created new static template {newtemplate}', server=server)
return {'notif': noti}, 201
@ns.route('/templates',
'/<server>/templates',
endpoint='templates_list')
@ -391,7 +479,7 @@ class NewTemplateSettings(Resource):
# if not clientconfdir:
# flash('Could not proceed, no \'clientconfdir\' find', 'warning')
# return redirect(request.referrer)
noti = bui.client.store_conf_cli(ImmutableMultiDict(), newtemplate, None, True, server)
noti = bui.client.store_conf_cli(ImmutableMultiDict(), newtemplate, None, True, False, server)
if server:
url = url_for('view.cli_settings', server=server, client=newtemplate, template=True)
else:
@ -416,6 +504,8 @@ class NewClientSettings(Resource):
parser = ns.parser()
parser.add_argument('newclient', required=True, help="No 'newclient' provided")
parser.add_argument('templates', help="Templates list", action='split')
parser.add_argument('statictemplate', help="Static template")
parser.add_argument('variables', help="Template variables")
@api.disabled_on_demo()
@api.acl_admin_or_moderator_required(message='Sorry, you don\'t have rights to access the setting panel')
@ -433,6 +523,10 @@ class NewClientSettings(Resource):
args = self.parser.parse_args()
newclient = args['newclient']
templates = [x for x in args.get('templates', []) if x]
statictemplate = args['statictemplate']
variables = json.loads(args['variables']) if args['variables'] else {}
variables['agent'] = server
variables['client'] = newclient
if not newclient:
self.abort(400, 'No client name provided')
@ -451,12 +545,18 @@ class NewClientSettings(Resource):
# flash('Could not proceed, no \'clientconfdir\' find', 'warning')
# return redirect(request.referrer)
data = MultiDict()
content = ''
if templates:
real_templates = {x['name']: x['value'] for x in parser._list_templates()}
if any(x not in real_templates for x in templates):
self.abort(400, 'Wrong template')
data.setlist('templates', [real_templates[x] for x in templates])
noti = bui.client.store_conf_cli(ImmutableMultiDict(data), newclient, None, agent=server)
if statictemplate:
statics = parser._list_static_templates()
for tpl in statics:
if tpl['name'] == statictemplate:
content = render_template_string(tpl['content'], **variables)
noti = bui.client.store_conf_cli(ImmutableMultiDict(data), newclient, None, content=content, agent=server)
if server:
url = url_for('view.cli_settings', server=server, client=newclient)
else:
@ -498,16 +598,20 @@ class ClientSettings(Resource):
parser_delete.add_argument('delcert', type=inputs.boolean, help='Whether to delete the certificate or not', default=False, nullable=True)
parser_delete.add_argument('keepconf', type=inputs.boolean, help='Whether to keep the conf or not', default=False, nullable=True)
parser_delete.add_argument('template', type=inputs.boolean, help='Whether we work on a template or not', default=False, nullable=True)
parser_delete.add_argument('statictemplate', type=inputs.boolean, help='Whether we work on a static template or not', default=False, nullable=True)
parser_delete.add_argument('delete', type=inputs.boolean, help='Whether we should remove the data as well or not', default=False, nullable=True)
parser_put = ns.parser()
parser_put.add_argument('newname', help='New name of the client/template')
parser_put.add_argument('template', type=inputs.boolean, help='Whether we work on a template or not', default=False, nullable=True)
parser_put.add_argument('statictemplate', type=inputs.boolean, help='Whether we work on a static template or not', default=False, nullable=True)
parser_put.add_argument('keepcert', type=inputs.boolean, help='Whether to keep the same certificate or not', default=False, nullable=True)
parser_put.add_argument('keepdata', type=inputs.boolean, help='Whether to keep the data or not', default=False, nullable=True)
parser_post = ns.parser()
parser_post.add_argument('template', type=inputs.boolean, help='Whether we work on a template or not', default=False, nullable=True)
parser_post.add_argument('statictemplate', type=inputs.boolean, help='Whether we work on a static template or not', default=False, nullable=True)
parser_get = ns.parser()
parser_get.add_argument('template', type=inputs.boolean, help='Whether we work on a template or not', default=False, nullable=True)
parser_get.add_argument('statictemplate', type=inputs.boolean, help='Whether we work on a static template or not', default=False, nullable=True)
@api.disabled_on_demo()
@api.acl_admin_or_moderator_required(message=_('Sorry, you don\'t have rights to access the setting panel'))
@ -528,7 +632,8 @@ class ClientSettings(Resource):
args = self.parser_post.parse_args()
template = args.get('template', False)
noti = bui.client.store_conf_cli(request.form, client, conf, template, server)
statictemplate = args.get('statictemplate', False)
noti = bui.client.store_conf_cli(request.form, client, conf, template, statictemplate, server)
# clear cache
cache.clear()
# clear client-side cache through the _extra META variable
@ -559,8 +664,9 @@ class ClientSettings(Resource):
pass
args = self.parser_get.parse_args()
template = args.get('template', False)
statictemplate = args.get('statictemplate', False)
parser = bui.client.get_parser(agent=server)
res = parser.read_client_conf(client, conf, template)
res = parser.read_client_conf(client, conf, template, statictemplate)
refresh()
# Translate the doc and placeholder API side
cache_keys = {
@ -627,6 +733,7 @@ class ClientSettings(Resource):
revoke = args.get('revoke', False)
keepconf = args.get('keepconf', False)
template = args.get('template', False)
statictemplate = args.get('statictemplate', False)
delete = args.get('delete', False)
if not keepconf:
@ -647,8 +754,9 @@ class ClientSettings(Resource):
bui.audit.logger.info(
f'deleted client configuration {client}, delete certificate: {delcert}, '
f'revoke certificate: {revoke}, keep a backup of the configuration: '
f'{keepconf}, delete data: {delete}, is template: {template}', server=server)
return parser.remove_client(client, keepconf, delcert, revoke, template, delete), 200
f'{keepconf}, delete data: {delete}, is template: {template} '
f'is static template: {statictemplate}', server=server)
return parser.remove_client(client, keepconf, delcert, revoke, template, statictemplate, delete), 200
@api.disabled_on_demo()
@api.acl_admin_or_moderator_required(message=_('Sorry, you don\'t have rights to access the setting panel'))
@ -677,6 +785,7 @@ class ClientSettings(Resource):
keepcert = args.get('keepcert', False)
keepdata = args.get('keepdata', False)
template = args.get('template', False)
statictemplate = args.get('statictemplate', False)
# clear the cache when we remove a client
cache.clear()
@ -695,8 +804,8 @@ class ClientSettings(Resource):
bui.audit.logger.info(
f'renaming client configuration {client} to {newname}, '
f'keep data: {keepdata}, keep certificate: {keepcert}, '
f'is template: {template}', server=server)
return parser.rename_client(client, newname, template, keepcert, keepdata), 200
f'is template: {template}, is static template: {statictemplate}', server=server)
return parser.rename_client(client, newname, template, statictemplate, keepcert, keepdata), 200
@ns.route('/path-expander',

View file

@ -1013,7 +1013,8 @@ class Burp(BUIbackend):
return []
return self.parser.read_server_conf(conf)
def store_conf_cli(self, data, client=None, conf=None, template=False, agent=None):
def store_conf_cli(self, data, client=None, conf=None, template=False,
statictemplate=False, content='', agent=None):
"""See :func:`burpui.misc.backend.interface.BUIbackend.store_conf_cli`"""
if not self.parser:
return []
@ -1021,7 +1022,7 @@ class Burp(BUIbackend):
conf = unquote(conf)
except:
pass
return self.parser.store_client_conf(data, client, conf, template)
return self.parser.store_client_conf(data, client, conf, template, statictemplate, content)
def store_conf_srv(self, data, conf=None, agent=None):
"""See :func:`burpui.misc.backend.interface.BUIbackend.store_conf_srv`"""

View file

@ -844,7 +844,8 @@ class BUIbackend(object, metaclass=ABCMeta):
raise NotImplementedError("Sorry, the current Backend does not implement this method!") # pragma: no cover
@abstractmethod
def store_conf_cli(self, data, client=None, conf=None, template=False, agent=None):
def store_conf_cli(self, data, client=None, conf=None, template=False,
statictemplate=False, content='', agent=None):
"""The :func:`burpui.misc.backend.interface.BUIbackend.store_conf_cli`
function works the same way as the
:func:`burpui.misc.backend.interface.BUIbackend.store_conf_srv` function

View file

@ -523,7 +523,8 @@ class NClient(BUIbackend):
"""
@implement
def store_conf_cli(self, data, client=None, conf=None, template=False, agent=None):
def store_conf_cli(self, data, client=None, conf=None, template=False,
statictemplate=False, content='', agent=None):
"""See :func:`burpui.misc.backend.interface.BUIbackend.store_conf_cli`"""
# serialize data as it is a nested dict
import hmac
@ -539,7 +540,14 @@ class NClient(BUIbackend):
data = ImmutableMultiDict(data.to_dict(False))
key = '{}{}'.format(self.password, 'store_conf_cli')
key = to_bytes(key)
pickles = to_unicode(b64encode(pickle.dumps({'data': data, 'conf': conf, 'client': client, 'template': template}, 2)))
pickles = to_unicode(
b64encode(
pickle.dumps(
{'data': data, 'conf': conf, 'client': client, 'template': template,
'statictemplate': statictemplate, 'content': content}, 2
)
)
)
bytes_pickles = to_bytes(pickles)
digest = to_unicode(hmac.new(key, bytes_pickles, hashlib.sha1).hexdigest())
data = {'func': 'store_conf_cli', 'args': pickles, 'pickled': True, 'digest': digest}

View file

@ -39,12 +39,17 @@ class Parser(Doc):
self._client_conf = {}
self._clients_conf = {}
self._templates_conf = {}
self._static_templates_conf = {}
self.clientconfdir = None
self.clientconfdir_mtime = None
self.templates = []
self.templates_dir = '.buitemplates'
self.templates_path = None
self.templates_mtime = None
self.static_templates = []
self.static_templates_dir = '.buistatictemplates'
self.static_templates_path = None
self.static_templates_mtime = None
self.filescache = {}
self._configs = {}
self.root = None
@ -102,6 +107,7 @@ class Parser(Doc):
if purge:
self._cleanup()
self._list_templates(True)
self._list_static_templates(True)
self._list_clients(True)
def _load_conf_srv(self):
@ -115,6 +121,12 @@ class Parser(Doc):
os.makedirs(self.templates_path, 0o755)
except OSError as exp:
self.logger.warning(str(exp))
self.static_templates_path = os.path.join(self.clientconfdir, self.static_templates_dir)
if not os.path.exists(self.static_templates_path):
try:
os.makedirs(self.static_templates_path, 0o755)
except OSError as exp:
self.logger.warning(str(exp))
def _load_conf_cli(self):
"""Load the client configuration file"""
@ -156,6 +168,22 @@ class Parser(Doc):
conf.parse()
self._templates_conf[template['name']] = conf
def _load_conf_static_templates(self, name=None, in_path=None):
"""Load all static templates configuration"""
if name:
templates = [{'name': name, 'value': in_path}]
else:
templates = self._list_static_templates(True)
for template in templates:
conf = self.server_conf.clone()
path = os.path.join(self.static_templates_path, template['name'])
if template['name'] not in self._static_templates_conf:
conf.add_file(path)
conf.set_default(path)
conf.parse()
self._static_templates_conf[template['name']] = conf
def _load_all_conf(self):
"""Load all configurations"""
self._cleanup()
@ -174,6 +202,11 @@ class Parser(Doc):
self._load_conf_templates(name, path)
return self._templates_conf[name]
def _new_static_template_conf(self, name, path):
"""Create new static template conf"""
self._load_conf_static_templates(name, path)
return self._static_templates_conf[name]
def _clientconfdir_changed(self):
"""Detect changes in clientconfdir"""
if not self.clientconfdir:
@ -196,6 +229,17 @@ class Parser(Doc):
return True
return False
def _static_templates_changed(self):
"""Detect changes in static_templates_dir"""
if not self.static_templates_path:
return False
mtime = os.path.getmtime(self.static_templates_path)
changed = mtime != self.static_templates_mtime
if changed:
self.static_templates_mtime = mtime
return True
return False
def _get_client(self, name, path):
"""Return client conf and refresh it if necessary
@ -220,9 +264,25 @@ class Parser(Doc):
self._load_conf_templates()
if name not in self._templates_conf:
return self._new_template_conf(name, path)
if self._templates_conf[name].changed:
self._templates_conf[name].parse()
return self._templates_conf[name]
template = self._templates_conf[name]
if template.changed:
template.parse()
return template
def _get_static_template(self, name, path=None):
"""Return static template conf and refresh it if necessary
:rtype: Config
"""
if self._static_templates_changed() and name not in self._static_templates_conf:
self._static_templates_conf.clear()
self._load_conf_static_templates()
if name not in self._static_templates_conf:
return self._new_static_template_conf(name, path)
template = self._static_templates_conf[name]
if template.changed:
template.parse()
return template
def _get_config(self, path, mode='cli'):
"""Return conf by it's path
@ -298,6 +358,35 @@ class Parser(Doc):
self.templates_mtime = os.path.getmtime(self.templates_path)
return res
def _list_static_templates(self, force=False):
res = []
if not self.clientconfdir or not os.path.isdir(self.static_templates_path):
return res
if self.static_templates and not force and \
not self._clientconfdir_changed() and \
not self._static_templates_changed():
return self.static_templates
for tpl in os.listdir(self.static_templates_path):
full_file = os.path.join(self.static_templates_path, tpl)
if os.path.isfile(full_file) and not tpl.startswith('.') and \
not tpl.endswith('~'):
try:
with open(full_file) as template:
res.append({
'name': tpl,
'value': os.path.join(self.static_templates_dir, tpl),
'content': template.read()
})
except OSError as exp:
self.logger.warning(str(exp))
self.static_templates = res
self.clientconfdir_mtime = os.path.getmtime(self.clientconfdir)
self.static_templates_mtime = os.path.getmtime(self.static_templates_path)
return res
def _get_server_path(self, name=None, fil=''):
"""Returns the path of the 'server *fil*' file"""
if not name:
@ -355,7 +444,7 @@ class Parser(Doc):
return self.openssl_auth.check_client_revoked(client)
def remove_client(self, client=None, keepconf=False, delcert=False, revoke=False,
template=False, delete=False):
template=False, statictemplate=False, delete=False):
"""See :func:`burpui.misc.parser.interface.BUIparser.remove_client`"""
res = []
revoked = False
@ -367,16 +456,20 @@ class Parser(Doc):
if not keepconf:
if template:
path = os.path.join(self.templates_path, client)
elif statictemplate:
path = os.path.join(self.static_templates_path, client)
else:
path = os.path.join(self.clientconfdir, client)
os.unlink(path)
res.append([NOTIF_OK, "'{}' successfully removed".format(client)])
removed = True
if client in self._clients_conf and not template:
if client in self._clients_conf and not (template or statictemplate):
del self._clients_conf[client]
elif template and client in self._templates_conf:
del self._templates_conf[client]
elif statictemplate and client in self._static_templates_conf:
del self._static_templates_conf[client]
if path in self.filescache:
del self.filescache[path]
@ -412,8 +505,8 @@ class Parser(Doc):
return res
def rename_client(self, client=None, newname=None, template=False, keepcert=False,
keepdata=False):
def rename_client(self, client=None, newname=None, template=False,
statictemplate=False, keepcert=False, keepdata=False):
"""See :func:`burpui.misc.parser.interface.BUIparser.rename_client`"""
res = []
if not client:
@ -424,6 +517,9 @@ class Parser(Doc):
if template:
path = os.path.join(self.templates_path, client)
newpath = os.path.join(self.templates_path, newname)
elif statictemplate:
path = os.path.join(self.static_templates_path, client)
newpath = os.path.join(self.static_templates_path, newname)
else:
data = self._get_server_path(client)
conf = self.clients_conf.get(client)
@ -436,16 +532,18 @@ class Parser(Doc):
except OSError as exp:
res.append([NOTIF_ERROR, str(exp)])
if client in self._clients_conf and not template:
if client in self._clients_conf and not (template or statictemplate):
del self._clients_conf[client]
elif template and client in self._templates_conf:
del self._templates_conf[client]
elif statictemplate and client in self._static_templates_conf:
del self._static_templates_conf[client]
if path in self.filescache:
del self.filescache[path]
self._refresh_cache()
if template:
if template or statictemplate:
return res
if keepdata:
@ -485,7 +583,7 @@ class Parser(Doc):
return res
def read_client_conf(self, client=None, conf=None, template=False):
def read_client_conf(self, client=None, conf=None, template=False, statictemplate=False):
"""
See :func:`burpui.misc.parser.interface.BUIparser.read_client_conf`
"""
@ -510,6 +608,9 @@ class Parser(Doc):
if template:
mconf = os.path.join(self.templates_path, client)
config = self._get_template(client, mconf)
elif statictemplate:
mconf = os.path.join(self.static_templates_path, client)
config = self._get_static_template(client, mconf)
else:
mconf = os.path.join(self.clientconfdir, client)
config = self._get_client(client, mconf)
@ -599,7 +700,13 @@ class Parser(Doc):
self.read_server_conf()
return self._list_templates()
def store_client_conf(self, data, client=None, conf=None, template=False):
def list_static_templates(self):
"""See :func:`burpui.misc.parser.interface.BUIparser.list_static_templates`"""
self.read_server_conf()
return self._list_static_templates()
def store_client_conf(self, data, client=None, conf=None, template=False,
statictemplate=False, content=''):
"""
See :func:`burpui.misc.parser.interface.BUIparser.store_client_conf`
"""
@ -608,21 +715,30 @@ class Parser(Doc):
if not conf and not client:
if template:
return [[NOTIF_ERROR, 'Sorry, no template defined']]
elif statictemplate:
return [[NOTIF_ERROR, 'Sorry, no static template defined']]
return [[NOTIF_ERROR, 'Sorry, no client defined']]
elif client and not conf:
if template:
if not self.templates_path:
return [[NOTIF_ERROR, 'Sorry, no template directory found']]
conf = os.path.join(self.templates_path, client)
elif statictemplate:
if not self.static_templates_path:
return [[NOTIF_ERROR, 'Sorry, no static template directory found']]
conf = os.path.join(self.static_templates_path, client)
else:
conf = os.path.join(self.clientconfdir, client)
ret = self.store_conf(data, conf, client, mode='cli', template=template)
ret = self.store_conf(data, conf, client, mode='cli', template=template,
statictemplate=statictemplate, content=content)
self._refresh_cache() # refresh client list
return ret
def store_conf(self, data, conf=None, client=None, mode='srv',
insecure=False, template=False):
insecure=False, template=False, statictemplate=False,
content=''):
"""See :func:`burpui.misc.parser.interface.BUIparser.store_conf`"""
ret = []
mconf = None
if not conf:
mconf = self.conf
@ -642,16 +758,42 @@ class Parser(Doc):
]
]
wrote_content = False
if content:
if os.path.exists(mconf):
ret.append([NOTIF_WARN, 'The file already exists, we won\'t override it'])
else:
try:
with open(mconf, 'w') as temp:
if not content.endswith('\n'):
content += '\n'
temp.write(content)
ret.append([NOTIF_OK, 'File successfully written from template'])
wrote_content = True
except IOError as exp:
ret.append([NOTIF_WARN, str(exp)])
check = False
if template:
conffile = self._get_template(client, mconf).get_file(mconf)
elif statictemplate:
conffile = self._get_static_template(client, mconf).get_file(mconf)
elif client:
conffile = self._get_client(client, mconf).get_file(mconf)
else:
conffile = self.server_conf.get_file(mconf)
check = True
ret = conffile.store_data(data, insecure)
if wrote_content and data:
for key in data.keys():
tmp = data.getlist(key)
if len(tmp) == 1:
conffile[key] = tmp[0]
else:
conffile[key] = tmp
ret += conffile.store(insecure=insecure)
elif not wrote_content:
ret += conffile.store_data(data, insecure)
if check:
clientconfdir = conffile.get('clientconfdir')

View file

@ -133,7 +133,8 @@ class BUIparser(object, metaclass=ABCMeta):
) # pragma: no cover
@abstractmethod
def store_client_conf(self, data, client=None, conf=None, template=False):
def store_client_conf(self, data, client=None, conf=None, template=False,
statictemplate=False, content=''):
""":func:`burpui.misc.parser.interface.BUIparser.store_client_conf` is
used by :func:`burpui.misc.backend.BUIbackend.store_conf_cli`.
@ -149,6 +150,12 @@ class BUIparser(object, metaclass=ABCMeta):
:param template: Is this file a template
:type template: bool
:param statictemplate: Whether we remove a static template
:type statictemplate: bool
:param content: What default content to put in the file
:type content: str
"""
raise NotImplementedError(
"Sorry, the current Parser does not implement this method!"
@ -156,7 +163,7 @@ class BUIparser(object, metaclass=ABCMeta):
@abstractmethod
def store_conf(self, data, conf=None, client=None, mode='srv',
insecure=False, template=False):
insecure=False, template=False, statictemplate=False, content=''):
""":func:`burpui.misc.parser.interface.BUIparser.store_conf` is used to
store the configuration from the web-ui into the actual configuration
files.
@ -181,6 +188,12 @@ class BUIparser(object, metaclass=ABCMeta):
:param template: Is it a template
:type template: bool
:param statictemplate: Whether we remove a static template
:type statictemplate: bool
:param content: What default content to put in the file
:type content: str
:returns: A list of notifications to return to the UI (success or
failure)
@ -248,6 +261,17 @@ class BUIparser(object, metaclass=ABCMeta):
"Sorry, the current Parser does not implement this method!"
) # pragma: no cover
@abstractmethod
def list_static_templates(self):
""":func:`burpui.misc.parser.interface.BUIparser.list_static_templates` is used
to retrieve a list of static templates with their absolute paths.
:returns: A list of templates
"""
raise NotImplementedError(
"Sorry, the current Parser does not implement this method!"
) # pragma: no cover
@abstractmethod
def is_client_revoked(self, client=None):
""":func:`burpui.misc.parser.interface.BUIparser.is_client_revoked` is
@ -264,7 +288,7 @@ class BUIparser(object, metaclass=ABCMeta):
@abstractmethod
def remove_client(self, client=None, keepconf=False, delcert=False, revoke=False,
template=False, delete=False):
template=False, statictemplate=False, delete=False):
""":func:`burpui.misc.parser.interface.BUIparser.remove_client` is used
to delete a client from burp's configuration.
@ -283,6 +307,9 @@ class BUIparser(object, metaclass=ABCMeta):
:param template: Whether we remove a template
:type template: bool
:param statictemplate: Whether we remove a static template
:type statictemplate: bool
:param delete: Whether to remove data as well
:type delete: bool
@ -295,8 +322,8 @@ class BUIparser(object, metaclass=ABCMeta):
) # pragma: no cover
@abstractmethod
def rename_client(self, client=None, newname=None, template=False, keepcert=False,
keepdata=False):
def rename_client(self, client=None, newname=None, template=False,
statictemplate=False, keepcert=False, keepdata=False):
""":func:`burpui.misc.parser.interface.BUIParser.rename_client` is used to
rename a client.
@ -309,6 +336,9 @@ class BUIparser(object, metaclass=ABCMeta):
:param template: Whether we remove a template
:type template: bool
:param statictemplate: Whether we remove a static template
:type statictemplate: bool
:param keepcert: Whether to keep using the same certificate or not
:type keepcert: bool
@ -324,7 +354,7 @@ class BUIparser(object, metaclass=ABCMeta):
) # pragma: no cover
@abstractmethod
def read_client_conf(self, client=None, conf=None, template=False):
def read_client_conf(self, client=None, conf=None, template=False, statictemplate=False):
""":func:`burpui.misc.parser.interface.BUIparser.read_client_conf` is
called by :func:`burpui.misc.backend.interface.BUIbackend.read_conf_cli`
in order to parse the burp-clients configuration files.

View file

@ -254,11 +254,13 @@ def cli_settings(server=None, client=None, conf=None):
client = client or request.args.get('client')
server = server or request.args.get('serverName')
template = request.args.get('template') or False
statictemplate = request.args.get('statictemplate') or False
return render_template(
'settings.html',
settings=True,
client_mode=True,
template=template,
statictemplate=statictemplate,
client=client,
server=server,
conf=conf,

View file

@ -187,6 +187,8 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$timeout', '$scrollspy', 'DTOp
{% if client -%}
{% if template -%}
$http.get('{{ url_for("api.client_settings", client=client, conf=conf, template=True, server=server) }}', { headers: { 'X-From-UI': true } })
{% elif statictemplate -%}
$http.get('{{ url_for("api.client_settings", client=client, conf=conf, statictemplate=True, server=server) }}', { headers: { 'X-From-UI': true } })
{% else -%}
$http.get('{{ url_for("api.client_settings", client=client, conf=conf, server=server) }}', { headers: { 'X-From-UI': true } })
{% endif -%}
@ -603,6 +605,25 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$timeout', '$scrollspy', 'DTOp
}
);
};
$scope.getStaticTemplatesList = function() {
api = '{{ url_for("api.static_templates_list", server=server) }}';
$http.get(
api,
{
headers: { 'X-From-UI': true },
}
).then(
function(response) {
var data = response.data;
$scope.raw.static_templates = data.result;
$scope.raw.static_templates.splice(0, 0, {'name': '{{ _("None") }}'});
$scope.all.static_templates = {};
_(data.result).forEach(function(r) {
$scope.all.static_templates[r.name] = {'value': r.value, 'variables': r.variables};
});
}
);
};
$scope.deleteFile = function() {
/* UX tweak: disable the submit button + change text */
submit = $('#btn-remove-file');
@ -650,6 +671,8 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$timeout', '$scrollspy', 'DTOp
type: 'PUT',
{% if template -%}
data: {template: true, newname: $('#newname').val() }
{% elif statictemplate -%}
data: {statictemplate: true, newname: $('#newname').val() }
{% else -%}
data: { newname: $('#newname').val(), keepcert: $('#keepcert').is(':checked'), keepdata: $('#keepdata').is(':checked') }
{% endif -%}
@ -688,6 +711,8 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$timeout', '$scrollspy', 'DTOp
type: 'DELETE',
{% if template -%}
data: { template: true }
{% elif statictemplate -%}
data: {statictemplate: true }
{% else -%}
data: { delcert: $('#delcert').is(':checked'), revoke: $('#revoke').is(':checked'), keepconf: $('#keepconf').is(':checked'), delete: $('#deldata').is(':checked') }
{% endif -%}
@ -739,11 +764,24 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$timeout', '$scrollspy', 'DTOp
e.preventDefault();
var form = $(e.target);
var templates = form.find('input[name="templates"]');
submit = form.find('button[type="submit"]');
sav = submit.html();
var variables = form.find('input[name="variables"]');
var submit = form.find('button[type="submit"]');
var disabled = [];
var sav = submit.html();
if ($scope.newclient.templates) {
templates.val($scope.newclient.templates.join(','));
}
if ($scope.newclient.statictemplate && $scope.newclient.statictemplate != 'None') {
temp = {};
_($('.static-variables').find('input')).forEach(function(raw) {
var input = $(raw);
console.log(input);
temp[input.attr('name')] = input.val();
input.attr('disabled', true);
disabled.push(input);
});
variables.val(JSON.stringify(temp));
}
submit.html('<i class="fa fa-fw fa-spinner fa-pulse" aria-hidden="true"></i>&nbsp;{{ _("Creating...") }}');
submit.attr('disabled', true);
$.ajax({
@ -766,6 +804,9 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$timeout', '$scrollspy', 'DTOp
.always(function() {
submit.attr('disabled', false);
submit.html(sav);
_(disabled).forEach(function(input) {
input.attr('disabled', false);
});
});
};
$scope.createTemplate = function(e) {
@ -798,6 +839,36 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$timeout', '$scrollspy', 'DTOp
submit.html(sav);
});
};
$scope.createStaticTemplate = function(e) {
/* we disable the 'real' form submission */
e.preventDefault();
var form = $(e.target);
submit = form.find('button[type="submit"]');
sav = submit.html();
submit.html('<i class="fa fa-fw fa-spinner fa-pulse" aria-hidden="true"></i>&nbsp;{{ _("Creating...") }}');
submit.attr('disabled', true);
$.ajax({
url: form.attr('action'),
type: 'PUT',
data: form.serialize()
})
.fail(buiFail)
.done(function(data) {
/* The server answered correctly but some errors may have occurred server
* side so we display them */
if (data.notif) {
notif(data.notif[0][0], data.notif[0][1]);
if (data.notif[0][0] == NOTIF_SUCCESS) {
$scope.getStaticTemplatesList();
notif(data.notif[1][0], data.notif[1][1], 20000);
}
}
})
.always(function() {
submit.attr('disabled', false);
submit.html(sav);
});
};
$scope.isNumber = function(key) {
return $scope.advanced && $scope.advanced[key] === 'integer';
};
@ -823,10 +894,12 @@ app.controller('ConfigCtrl', ['$scope', '$http', '$timeout', '$scrollspy', 'DTOp
$scope.loadConfig();
$scope.getClientsList();
$scope.getTemplatesList();
$scope.getStaticTemplatesList();
}]);
{{ macros.page_length('#table-list-clients') }}
{{ macros.page_length('#table-list-templates') }}
{{ macros.page_length('#table-list-static-templates') }}
$(document).ready(function () {
$('#config-nav a').click(function (e) {

View file

@ -18,6 +18,8 @@
{% else -%}
{% if template -%}
<li class="active">{{ _('Template %(name)s on %(server)s', name=client, server=server) }}</li>
{% elif statictemplate -%}
<li class="active">{{ _('Static template %(name)s on %(server)s', name=client, server=server) }}</li>
{% else -%}
<li class="active">{{ _('%(client)s on %(server)s', client=client, server=server) }}</li>
{% endif -%}
@ -40,6 +42,8 @@
{% else -%}
{% if template -%}
<li class="active">{{ _('Template %(name)s', name=client) }}</li>
{% elif statictemplate -%}
<li class="active">{{ _('Static template %(name)s', name=client) }}</li>
{% else -%}
<li class="active">{{ client }}</li>
{% endif -%}
@ -64,6 +68,7 @@
{% endif -%}
<li {% if is_moderator and not is_admin %}class="active"{% endif %}><a href="#clients" data-toggle="tab" aria-expanded="false">{{ _('Clients') }}</a></li>
<li><a href="#list-templates" data-toggle="tab" aria-expanded="false">{{ _('Templates') }}</a></li>
<li><a href="#list-static-templates" data-toggle="tab" aria-expanded="false">{{ _('Static templates') }}</a></li>
</ul>
<div id="config-tab-content" class="tab-content">
{% if (not is_moderator and is_admin) or client_mode -%}
@ -80,6 +85,8 @@
{% if client -%}
{% if template -%}
<form class="form-horizontal" action="{{ url_for('api.client_settings', client=client, conf=conf, template=template, server=server) }}" method="POST" ng-submit="submit($event)" name="setSettings" onbeforeunload>
{% elif statictemplate -%}
<form class="form-horizontal" action="{{ url_for('api.client_settings', client=client, conf=conf, statictemplate=statictemplate, server=server) }}" method="POST" ng-submit="submit($event)" name="setSettings" onbeforeunload>
{% else -%}
<form class="form-horizontal" action="{{ url_for('api.client_settings', client=client, conf=conf, server=server) }}" method="POST" ng-submit="submit($event)" name="setSettings" onbeforeunload>
{% endif -%}
@ -486,7 +493,32 @@
</ui-select-choices>
</ui-select>
</div>
<div class="form-group">
<ui-select ng-model="newclient.statictemplate" ng-disabled="!raw.static_templates" theme="bootstrap">
<ui-select-match placeholder="{{ _('Select a static template') }}">
{% raw %}
{{ $select.selected.name }}
{% endraw %}
</ui-select-match>
<ui-select-choices repeat="value.name as value in raw.static_templates | filter: $select.search">
<div ng-bind-html="value.name | highlight: $select.search"></div>
</ui-select-choices>
</ui-select>
</div>
<div class="static-variables" ng-if="newclient.statictemplate && newclient.statictemplate != 'None'">
<div class="form-group row" ng-repeat="val in all.static_templates[newclient.statictemplate]['variables'] track by $index">
{% raw -%}
<label for="static-variables_{{ val }}" class="col-lg-2 control-label">{{ val }}</label>
<div class="col-lg-10">
<input type="text" class="form-control" id="static-variables_{{ val }}" name="{{ val }}"
{% endraw -%}
placeholder="{{ _('Enter value') }}">
</div>
</div>
</div>
<input type="hidden" name="templates">
<input type="hidden" name="variables">
<input type="hidden" name="statictemplate" ng-value="newclient.statictemplate">
</fieldset>
</form>
</div>
@ -525,6 +557,40 @@
</form>
</div>
</div>
<div class="tab-pane fade" id="list-static-templates">
<div style="padding-top: 80px; margin-top: -45px;"></div>
<div id="table-static-templates" class="table-responsive row">
<table class="table table-striped table-hover nowrap" width="100%" datatable="ng" dt-options="dtOptions" dt-column-defs="dtColumnDefs" id="table-list-static-templates">
<thead>
<tr>
<th>{{ _('Name') }}</th><th>{{ _('Path') }}</th><th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="template in raw.static_templates" ng-if="template.name != 'None'">
{% raw -%}
<td>{{ template.name }}</td>
<td>{{ template.value }}</td>
{% endraw -%}
<td><a href="{{ url_for("view.cli_settings", server=server) }}?client={{ '{{' }} template.name {{ '}}' }}&statictemplate=true" class="btn btn-info btn-xs no-link pull-right" title="{{ _('edit') }}"><i class="fa fa-pencil" aria-hidden="true"></i></a></td>
</tr>
</tbody>
</table>
</div>
<div class="row well">
<form action="{{ url_for('api.new_static_template', server=server) }}" method="POST" ng-submit="createStaticTemplate($event)">
<fieldset>
<Legend>{{ _('Create new static template') }}</Legend>
<div class="input-group">
<input class="form-control" type="text" name="newstatictemplate" id="newstatictemplate" placeholder="{{ _('Create new static template') }}">
<span class="input-group-btn">
<button class="btn btn-success" type="submit"><i class="fa fa-plus" aria-hidden="true"></i></button>
</span>
</div>
</fieldset>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -68,6 +68,14 @@ v0.7.0
from any rule with the ``exclude`` keyword. See the
`BASIC ACL <advanced_usage.html#basic-acl>`__ documentation for details.
- **New** - You can now create *static templates* which support `jinja2
variables <https://jinja.palletsprojects.com/en/2.10.x/templates/#variables>`_
format. These templates are applied only *once* at the creation of a new
client if you choose to use them. Also note there are two default variables:
``{{client}}`` and ``{{agent}}`` injected while rendering them which contain
respectively the name of the *client* being created and the name of the
*agent* you are working on.
v0.6.0
------